4

由于我对单元测试和 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 回答 1

8

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

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

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 或其他东西。

希望有这么多有用的东西...

于 2012-11-15T20:29:23.090 回答