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

本文关键字:创建 单元测试 方法 返回 列表 | 更新日期: 2023-09-27 18:21:48

我有这个方法:

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;
            }
        }

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

我的第一个问题是:

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

  2. 如果是这样,我将如何测试它?

这是我到目前为止所拥有的:

    [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?
        }
    }

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

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

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

  • RepositoryHelper.GetTake2Repository(((扔?(瑕疵问题(
  • ProjectCrewsByProjectSpec 构造函数抛出?(依赖关系问题(
  • 代表。GetList(spec(抛出?剑道(它是剑道,对吧?(依赖关系问题(
  • ToDataSourceResult(( throwing?(行为问题(

单元测试就是在与依赖关系完全隔离的情况下测试事物,所以目前我想说它更像是一个集成测试,你并不真正关心系统如何交互,你只想确保对于给定的projectID,seasonId和episodeId你得到预期的结果 - 在这种情况下,真正测试的是代表。GetList(( 方法与 .到数据源结果扩展。

现在集成测试非常有用,并且 100% 需要作为测试驱动方法的一部分,如果这是您真正想要做的,那么您正在做正确的事情。(我把这个放进去,然后期待回来;我把它拿回来了吗?(

但是,如果要对此

代码进行单元测试(特别是,如果要对类的 GetProjectBySpec 方法进行单元测试(,则必须按照@jimmy_keen提到的操作并重构它,以便可以测试 GetProjectBySpec 的行为。例如,这是我刚刚发明的特定行为,当然您的行为可能会有所不同:

  • 如果输入错误,则抛出 ArgumentException
  • 创建新的 ProjectCrewsByProjectSpec 对象
  • 呼叫代表获取列表并将规范传递给它
  • 返回非空数据源结果

为了能够测试 GetProjectBySpec 执行上述列表中的所有操作,您需要做的第一件事是重构它,以便它不会创建自己的依赖项 - 相反,您通过依赖注入为它提供所需的依赖项。

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

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

但是,让我们假设您可以这样定义接口:(请耐心等待我永远不会 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.

希望能给你一些帮助...当然,您可以重构测试类以避免大量模拟设置,并将其全部放在一个位置,以将代码行数保持在最低限度。

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

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

在您的情况下,这可能取决于//big query...部分正在做什么。如果是过滤/转换从存储库返回的结果,则应对其进行测试。

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

  • RepositoryHelper.GetTake2Repository应该包装在接口中,作为依赖项注入,稍后在单元测试中模拟
  • 如果new ProjectCrewsByProjectSpec创建复杂对象,则可能需要改用工厂

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

我这样写测试:

  1. 为代码中的每个路径编写一个测试。
  2. 为边界条件编写检验。例如:列表中的零项、一项或两项。参数错误等。
  3. 写阴性测试。这些是最难的,因为你可以编写无数个无用的测试。一个很好的例子是检查不应该更改的内容是否未更改。

祝你好运

这是更简单的方法,您也可以通过它测试其中的数据。从您离开的位置进行修改。

[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);          
        }
}