【问题标题】:Methods for Adding Data to Mock DBs in C# Unit Tests在 C# 单元测试中向 Mock DB 添加数据的方法
【发布时间】:2012-11-15 17:35:23
【问题描述】:

这篇文章更多是为了引发讨论,因为我对单元测试和 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&lt;Survey&gt;(它们很可能是List&lt;Survey&gt;)。但是,我将在未来扩展接口和这些类的功能,以根据添加到 LINQ 查询的某些标准过滤掉假/有效系列,这就是我让存储库实现 IQueryable&lt;&gt; 的原因。这是模拟代码,旨在传达我所想的基本原则。

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

  1. 您对我在这种情况下可以采取的替代方法有什么建议吗?
  2. 您过去使用过哪些方法,您喜欢/不喜欢它们的哪些方面?您发现哪个最容易维护?
  3. 鉴于我发布的内容,您是否注意到我的一般单元测试方法存在缺陷?有时我觉得我编写的单元测试试图涵盖太多内容,而不是简洁、优雅和中肯。

这是一种公开的讨论。请记住,这是我写过的第一组单元测试(不过,我已经阅读了大量关于该主题的文献)。

【问题讨论】:

    标签: c# unit-testing mocking tdd


    【解决方案1】:

    我认为你在一个很好的轨道上。

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

    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 之类的,否则我个人不会再使用基于文件的方法。

    希望所有这些中都有一些有用的东西......

    【讨论】:

    • 非常好!我喜欢你创建不同类的方法,这些类可以根据某些标准构建模拟小马。您的反馈正是我想要的:)
    • 感谢@Greg 的示例。我实际上已经在我的项目中实现了类似的东西,并且根据您的回答,我已经将解决​​方案扩展了一点,所以现在它考虑了特定的场景。干杯
    猜你喜欢
    • 2020-12-27
    • 2020-07-15
    • 1970-01-01
    • 2017-09-15
    • 2019-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多