在C#单元测试中向Mock DB添加数据的方法

时间:2012-11-15 17:35:24

标签: c# unit-testing mocking tdd

这篇文章意味着更多的讨论 - 首发,因为我对单元测试和TDD有点新。

我目前正在为一个与多个数据库交互的.NET进程编写一些单元测试,并且正在使用模拟数据库上下文来尝试覆盖我的测试中的不同边缘情况,验证程序本身中的异常处理,以及其他一些事情。 。话虽这么说,我的一些单元测试使用有效数据,而其他测试没有。

在向模拟数据库上下文添加有效/虚假数据时,我正在寻找有关建议的最佳做法的反馈。我见过人们通过多种方式做到这一点(例如 - 实现存储库模式,将模拟数据添加到.csv文件中并使它们成为项目的一部分等等。)

我目前正在考虑使用存储库模式将Survey个对象添加到目标数据库中的Surveys表。

首先,我有界面:

public interface ISurveyRepository
{
   IQueryable<Survey> SurveySeries { get; }
}

这是针对单元测试所需的模拟假/有效数据存储库实现的

class FakeSurveyRepository : ISurveyRepository
{
   private static IQueryable<Survey> fakeSurveySeries = new List<Survey> {
      new Survey { id = 1, SurveyName="NotValid1", SurveyData="<data>fake</data>"},
      new Survey { id = 2, SurveyName="NotValid2", SurveyData="<data>super fake</data>"},
      .........,
      new Survey {id = 10, SurveyName="NotValid10", SurveyData="<data>the fakest</data>" }       
   }.AsQueryable();

   public IQueryable<Survey> SurveySeries 
   { 
      get { return fakeSurveySeries; }
   }
}
// RealSurveyRepository : ISurveyRepository is similar to this, but with "good" data

然后我有一个类通过传递对构造函数中的系列的引用来使用伪数据/有效数据的数据:

public class SurveySeriesProcessor
{
   private ISurveyRepository surveyRepository;

   public SurveySeriesProcessor( ISurveyRepository surveyRepository )
   {
       this.surveyRepository = surveyRepository;
   }

   public IQueryable<Survey> GetSurveys()
   {
      return surveyRepository.SurveySeries
   }
} 

然后可以在我的测试中使用这些对象,例如:

[TestClass]
public class SurveyTests
{
    [TestMethod]
    WhenInvalidSurveysFound_SurveyCopierThrowsInvalidSurveyDataErrorForEach()
    {
       // create mocking DB context and add fake data
       var contextFactory = new ContextFactory( ContextType.Mocking );
       var surveySeriesProcessor = new SurveySeriesProcessor( new FakeSurveyRepository() );

       foreach(Survey surveyRecord in surveySeriesProcessor.GetSurveys() )
       {
          contextFactory.TargetDBContext.Surveys.AddObject( surveyRecord );
       }
       // instantiate object being tested and run it against fake test data
       var testSurveyCopier = new SurveyCopier( contextFactory );
       testSurveyCopier.Start();
       // test behavior
       List<ErrorMessage> errors = testSurveyCopier.ErrorMessages;
       errors.Count.ShouldEqual( surveySeriesProcessor.GetSurveys().Count );
       foreach(ErrorMessage errMsg in errors)
       {
          errMsg.ErrorCode.ShouldEqual(ErrorMessage.ErrorMessageCode.InvalidSurveyData);
       }
    }
}

注意:我意识到在提供的示例代码中,我不一定需要使实现ISurveyRepository的类将系列返回为IQueryable<Survey>(它们很可能是List<Survey> )。但是,我将在未来扩展接口和这些类的功能,以根据添加到LINQ查询的某些条件过滤掉伪/有效系列,这就是我使存储库实现IQueryable<>的原因。这是一个模拟代码,旨在传达我正在思考的基本原则。

考虑到所有这一切,我要问的是:

  1. 对于我在这种情况下可以采取的替代方法,你有什么建议吗?
  2. 过去你采用了哪些方法,你喜欢/不喜欢什么?你发现哪个最容易维护?
  3. 鉴于我发布的内容,您是否注意到我对单元测试的一般方法存在缺陷?有时候我觉得我写的单元测试试图覆盖太多的地面而不是简洁,优雅和对点。
  4. 这是一个公开的讨论。请记住,这是我写过的第一套单元测试(不过我已经阅读过相当多的关于这个主题的文献)。

1 个答案:

答案 0 :(得分:3)

我认为你的情况很好。

就个人而言,在相同情况下,如果我正在处理存储库样式模式,

public interface IRepository<T>
{
    IEnumerable<T> GetAll();
}


public class PonyRepository : IRepository<Pony>
{
    IEnumerable<Pony> GetAll();
}

为了实际提供我需要的数据,我通常会创建一个TestObjects或TestFakes类来按需提供所需的数据。

public class FakeStuff
{
     public static IEnumerable<Pony> JustSomeGenericPonies(int numberOfPonies)
     {
        // return just some basic list
         return new List<Pony>{new Pony{Colour = "Brown", Awesomeness = AwesomenessLevel.Max}};

         // or could equally just go bananas in here and do stuff like...
         var lOfP = new List<Pony>();
         for(int i = 0; i < numberOfPonies; i++)
         {
             var p = new Pony();
             if(i % 2 == 0) 
             {
                 p.Colour = "Gray";
             }
             else
             {
                 p.Colour = "Orange"; 
             }

             lOfP.Add(p);
         }

         return lOfP;
     }
}

用这样测试:

[Test]
public void Hello_I_Want_to_test_ponies()
{
    Mock<IRepository<Pony> _mockPonyRepo = new Mock<IRepository<Pony>>();
    _mockPonyRepo.SetUp(m => m.GetAll()).Returns(FakeStuff.JustSomeGenericPonies(50));

    // Do things that test using the repository
}

因此,通过将虚假数据保留在存储库之外并将其保存在自己的位置,这可以提供虚假数据的可重用性,这意味着我可以在测试需要一个小马列表的任何地方调用此小马列表,而不仅仅是存储库的位置参与。

如果我需要特定测试用例的特定数据,我将实现类似于你的东西,但是对于特定的Fake存储库的用途更加明确:

public class FakePonyRepositoryThatOnlyReturnsBrownPonies : IRepository<Pony>
{
    private List<Pony> _verySpecificAndNotReusableListOfOnlyBrownPonies = new List....

    public IEnumerable<Pony> GetAll()
    {
        return _verySpecificAndNotReusableListOfOnlyBrownPonies;
    }
}

public class FakePonyRepositoryThatThrowsExceptionFromGetAll : IRepository<Pony>
{
    public IEnumerable<Pony> GetAll()
    {
        throw new OmgNoPoniesException();
    }
}

您也提到了CSV文件 - 这可能是可行的(过去曾使用过XML),但我认为以CSV或XML格式保存虚假数据只是使用本地化数据库保存数据的更糟糕版本SQL CE或一些等价物。但是,这些都不太可维护,而且至关重要的是,在单元测试方面,比使用内存中的假对象慢。我个人不会再使用基于文件的方法,除非我专门测试序列化或IO或其他东西。

希望在这一切中有一些有用的东西......