如何为返回列表的方法创建适当的单元测试?

Posted

技术标签:

【中文标题】如何为返回列表的方法创建适当的单元测试?【英文标题】:How do you create a proper unit test for a method that returns a list? 【发布时间】:2013-07-08 23:13:28 【问题描述】:

我有这个方法:

public DataSourceResult GetProjectBySpec(int projectId, int seasonId, int episodeId)
        
            using (var rep = RepositoryHelper.GetTake2Repository<ITake2RepositoryBase>())
            
                var spec = new ProjectCrewsByProjectSpec(projectId, seasonId, episodeId);

                var personList = rep.GetList<ProjectDGACrew>(spec).Select(p => new
                
                    //big query...
                    .ToDataSourceResult();

                return personList;
            
        

我需要为此创建一个单元测试。

我的第一个问题是:

    我要测试什么?我是否只测试该方法是否返回列表?

    如果是这样,我将如何进行测试?

这是我目前所拥有的:

    [TestClass]
    public class CrewControllerTest
    
        [TestMethod]
        public void GetProjectCrewsBySpecTest()
        
            // arrange
            int projectId = 1;
            int seasonId = 2;
            int episodeId = 3;

            // act
            var crewController = new CrewController();
            DataSourceResult dsr = crewController.GetProjectCrewsBySpec(1, 2, 3);

            // assert
            // what or how do I assert here? Am I just checking whether "dsr" is a list? How do I do that?
        
    

【问题讨论】:

我也是一个单元测试新手,但我的假设是当你输入这些输入时,你已经知道应该从方法中得到什么。将其与您实际返回的内容进行比较,以确定单元测试是否成功。 所以基本上测试查询毫无价值,不是吗?因为数据随时都在变化。 如果是这种情况,您可能必须模拟数据库才能每次都获得可靠的回报。由于我不是积极的,我只会 +1 你的问题,并等待更有知识的人来权衡。:) 谢谢!刚开始学习如何进行单元测试,所以我需要一些帮助,哈哈 【参考方案1】:

我也不是专家,而且我只是做了一小段时间 TDD,所以用一勺盐来接受我在这个漫无边际的答案中写的内容 :) 我相信其他人可以指出我是否已经犯了任何非常严重的错误或将您指向错误的方向...

我不确定您的测试是否真的是单元测试,因为它正在执行多个依赖项。假设您运行此测试并从方法中抛出异常。 这个异常是否来自

RepositoryHelper.GetTake2Repository()) 投掷? (依赖问题) ProjectCrewsByProjectSpec 构造函数抛出? (依赖问题) rep.GetList(spec) 抛出?剑道(它是 剑道,对吧?)(依赖问题) ToDataSourceResult() 抛出? (行为问题)

单元测试就是在完全独立于它们的依赖项的情况下测试事物,所以目前我想说它更像是一个集成测试,您并不真正关心系统如何交互,您只想确保对于给定的 projectID、seasonId 和 episodeId,您可以获得预期的结果 - 在这种情况下,真正测试的是 rep.GetList()方法结合 .ToDataSourceResult 扩展。

现在集成测试非常有用,并且 100% 需要作为测试驱动方法的一部分,如果那是你真正想做的,那么你做的就是正确的。(我把 这个 em> 并期待 that 回来;我得到 that 回来了吗?)

但是如果你想单元测试这段代码(特别是,如果你想对你的类的 GetProjectBySpec 方法进行单元测试)你必须按照@jimmy_keen 提到的那样做并重构它,以便您可以测试 GetProjectBySpec 的行为。 例如这是我刚刚发明的一个特定行为,当然你的可能会有所不同:

如果输入错误,抛出 ArgumentException 创建一个新的 ProjectCrewsByProjectSpec 对象 调用 rep.GetList 并将规范传递给它 返回非 null DataSourceResult

为了能够测试 GetProjectBySpec 是否完成了上面列表中的所有事情,您需要做的第一件事是重构它,以便它不会创建自己的依赖项 - 相反,您给它提供依赖项它需要,通过Dependency Injection。

当您通过接口注入时,DI 确实效果最佳,因此在任何提供此方法的类中,该类的构造函数都应采用例如 IRepositoryHelper 的实例并将其存储在私有只读成员中.它还应该采用 IProjectCrewsByProjectSpecFactory 的实例,您将使用它来创建规范。现在,既然您想用这些依赖项测试 GetProjectBySpec 的实际操作,那么您将使用诸如 Moq 之类的模拟框架,除了下面的示例外,我不会在这里介绍它。 p>

如果这些类目前都没有实现这样的接口,那么只需使用 Visual Studio 根据类定义为您提取接口定义。如果它们是您无法控制的第 3 方类,这可能会很棘手。

但是让我们假设您可以这样定义接口:(请耐心等待我从来没有 100% 使用过的通用 位,我相信比我更聪明的人可以告诉您所有“T "应该去...) 下面的代码没有经过测试,也没有检查错别字!

public interface IRepositoryHelper<ProjectDGACrew>

  IList<ProjectDGACrew> GetList(IProjectCrewsByProjectSpecFactory spec);


public interface IProjectCrewsByProjectSpecFactory 

 ProjectDGACrew Create(int projectId, int seasonId, int episodeId);

您的代码最终会看起来像这样:

//somewhere in your class definition
private readonly IRepositoryHelper<T> repo;
private readonly IProjectCrewsByProjectSpecFactory pfactory;
//constructor
public MyClass(IRepositoryHelper<ProjectDGACrew> repo, IProjectCrewsByProjectSpecFactory pfactory)

this.repo = repo;
this.pfactory=pfactory;

//method to be tested
public DataSourceResult GetProjectBySpec(int projectId, int seasonId, int episodeId)

            var spec = pfactory.Create(projectId, seasonId, episodeId);
            var personList = repo.GetList(spec).Select(p => new
                //big query...).ToDataSourceResult();
                return personList;

现在你有 4 个测试方法要编写:

[TestMethod]
[ExepctedException(typeof(ArgumentException)]
public void SUT_WhenInputIsBad_ThrowsArgumentException()

    var sut = new MyClass(null,null); //don't care about our dependencies for this check
    sut.GetProjectBySpec(0,0,0); //or whatever is invalid input for you.
    //don't care about the return, only that the method throws.




[TestMethod]
public void SUT_WhenInputIsGood_CreatesProjectCrewsByProjectSpec()

  //create dependencies using Moq framework.
  var pf= new Mock<IProjectCrewsByProjectSpecFactory>();
  var repo = new Mock<IRepository<ProjectDgaCrew>>();
  //setup such that a call to pfactory.Create in the tested method will return nothing
  //because we actually don't care about the result - only that the Create method is called.
  pf.Setup(p=>p.Create(1,2,3)).Returns<ProjectDgaCrew>(new ProjectDgaCrew());
  //setup the repo such that any call to GetList with any ProjectDgaCrew object returns an empty list
//again we do not care about the result. 
//This mock dependency is only being used here
//to stop an exception being thrown from the test method
//you might want to refactor your behaviours
//to specify an early exit from the function when the factory returns a null object for example.
   repo.Setup(r=>r.GetList(It.IsAny<ProjectDgaCrew>()).Returns<IList<ProjectDGACrew>>(new List<ProjectDgaCrew>());
  //create our System under test, inject our mock objects:
   var sut = new MyClass(repo,pf.Object);
   //call the method:
   sut.GetProjectBySpec(1,2,3);
   //and verify that it did indeed call the factory.Create method.
    pf.Verify(p=>p.Create(1,2,3),"pf.Create was not called with 1,2,3");              

        public void SUT_WhenInputIsGood_CallsRepoGetList() //you get the idea
        public void SUT_WhenInputIsGood_ReturnsNonNullDataSourceResult()//and so on.

希望能给您一些帮助...当然,您可以重构您的测试类以避免大量的模拟设置,并将所有这些设置在一个地方以将代码行数保持在最低限度。

【讨论】:

【参考方案2】:

单元测试通常(应该)测试类客户端所理解的合同。当您的代码客户调用.GetProjectBySpec(1, 2, 3) 时,他期望发生什么?单元测试应该回答这个问题:

当存储库中有 5 个项目时(ABCDE ) 我用参数 123 调用 GetProjectBySpec,我应该得到项目 BC

在您的情况下,这可能取决于//big query... 部分中正在执行的操作。如果它是从存储库返回的结果的过滤/转换,那么这就是您应该测试的内容。

请注意,您可能需要更改一些内容才能使此测试隔离(与存储库/数据库):

RepositoryHelper.GetTake2Repository 应该封装在接口中,injected 作为依赖,mocked 稍后在单元测试中 如果new ProjectCrewsByProjectSpec 创建复杂对象,您可能需要改用factory

当您模拟存储库时,您只需指示您的模拟在使用匹配的spec 参数调用它时返回一些预先已知的项目列表。然后您的单元测试可以验证从GetProjectBySpec 返回的数据是否符合您的期望。

【讨论】:

【参考方案3】:

我这样写测试:

    为通过代码的每条路径编写一个测试。 为边界条件编写测试。例如:您的列表中的零个、一个或两个项目。参数错误等。 编写否定测试。这些是最难的,因为您可以编写无限数量的无用测试。一个很好的例子是检查不应该改变的东西没有改变。

祝你好运

【讨论】:

【参考方案4】:

这是一种更简单的方法,您也可以通过它测试其中的数据。从你离开的地方修改。

[TestClass]
    public class CrewControllerTest
    
        [TestMethod]
        public void GetProjectCrewsBySpecTest()
        
          // arrange
          const String ExpectedOutput = "";
          int projectId = 1;
          int seasonId = 2;
          int episodeId = 3;

          // act
          var crewController = new CrewController();
          var resultList= crewController.GetProjectCrewsBySpec(1, 2,3) as DataSourceResult;
          var someInsideData = resultlist.FirstOrDefault().GetType().GetProperty("PropertyName").GetValue(resultList.FirstOrDefault(),null);

          // assert
          Assert.AreEqual(someInsideData , ExpectedOutput);          
        

【讨论】:

以上是关于如何为返回列表的方法创建适当的单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 SQL 语句创建单元测试?

如何为调用 reduxjs 的 mapStateToProps 的反应组件编写单元测试?

如何为 UserDefaults 编写单元测试

如何为 iPhone 应用程序运行和调试单元测试

Spring Boot:如何为删除其余模板编写单元测试用例

如何为 Xamarin 库项目(iOS 和 Android)运行单元测试?