我也不是专家,而且我只做了一小段时间 TDD,所以用一勺盐来拿我在这个漫无边际的答案中写的东西:) 我相信其他人可以指出我是否真的做过严重的错误或将您指向错误的方向...
我不确定您的测试是否真的是单元测试,因为它正在执行多个依赖项。假设您运行此测试并从方法中抛出异常。这个异常是否来自
- RepositoryHelper.GetTake2Repository()) 抛出?(依赖问题)
- ProjectCrewsByProjectSpec 构造函数抛出?(依赖问题)
- rep.GetList(spec) 抛出?剑道(它是剑道,对吧?)(依赖问题)
- ToDataSourceResult() 抛出?(行为问题)
单元测试就是完全独立于它们的依赖项来测试事物,所以目前我想说它更像是一个集成测试,你并不真正关心系统如何交互,你只想确保对于给定的projectID、seasonId 和 episodeId 你会得到预期的结果——在这种情况下,真正测试的是rep.GetList()方法与 . ToDataSourceResult扩展。
现在集成测试非常有用,并且 100% 需要作为测试驱动方法的一部分,如果那是你真正想做的,那么你做对了。(我把它放进去并期待它回来;我拿回来?)
但是如果你想对这段代码进行单元测试(特别是,如果你想对你的类的GetProjectBySpec方法进行单元测试),你必须按照@jimmy_keen 提到的那样做并重构它,以便你可以测试 GetProjectBySpec 的行为。例如,这是我刚刚发明的一个特定行为,当然你的可能会有所不同:
- 如果输入错误,则抛出 ArgumentException
- 创建一个新的 ProjectCrewsByProjectSpec 对象
- 调用 rep.GetList 并将 spec 传递给它
- 返回一个非空的 DataSourceResult
为了能够测试 GetProjectBySpec 是否执行上述列表中的所有操作,您需要做的第一件事是重构它,以便它不会创建自己的依赖项 - 相反,您给它提供它需要的依赖项,通过依赖注入。
当您通过接口注入时,DI 确实效果最好,因此在任何提供此方法的类中,您的该类的构造函数都应该采用例如IRepositoryHelper的实例并将其存储在私有只读成员中。它还应该采用IProjectCrewsByProjectSpecFactory的实例,您将使用它来创建规范。现在,既然您想测试 GetProjectBySpec 对这些依赖项的实际作用,那么您将使用Moq之类的模拟框架,除了下面的示例外,我不会在这里介绍它。
如果这些类目前都没有实现这样的接口,那么只需使用 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.
希望能给你一些帮助……当然,你可以重构你的测试类来避免大量的模拟设置,并将它们都放在一个地方,以将代码行保持在最低限度。