我一直在研究the NuGetGallery中单元测试的方式。我观察到,当测试控制器时,服务类被模拟。这对我来说很有意义,因为在测试控制器逻辑时,我不想担心下面的架构层。在使用这种方法一段时间之后,我注意到当我的服务类发生变化时,我经常在我的控制器测试中修复我的模拟。为了解决这个问题,在没有咨询比我更聪明的人的情况下,我开始编写这样的测试(不用担心,我还没有那么远):
public class PersonController : Controller
{
private readonly LESRepository _repository;
public PersonController(LESRepository repository)
{
_repository = repository;
}
public ActionResult Index(int id)
{
var model = _repository.GetAll<Person>()
.FirstOrDefault(x => x.Id == id);
var viewModel = new VMPerson(model);
return View(viewModel);
}
}
public class PersonControllerTests
{
public void can_get_person()
{
var person = _helper.CreatePerson(username: "John");
var controller = new PersonController(_repository);
controller.FakeOutContext();
var result = (ViewResult)controller.Index(person.Id);
var model = (VMPerson)result.Model;
Assert.IsTrue(model.Person.Username == "John");
}
}
我想这将是集成测试,因为我使用的是真正的数据库(我更喜欢内存)。我通过将数据放入我的数据库开始我的测试(每个测试在一个事务中运行,并在测试完成时回滚)。然后我调用我的控制器,我真的不在乎它如何从数据库中检索数据(通过存储库或服务类)只是要发送到视图的模型必须有我放入数据库的记录,也就是我的断言。这种方法的一个很酷的事情是,很多时候我可以继续添加更多层的复杂性,而无需更改我的控制器测试:
public class PersonController : Controller
{
private readonly LESRepository _repository;
private readonly PersonService _personService;
public PersonController(LESRepository repository)
{
_repository = repository;
_personService = new PersonService(_repository);
}
public ActionResult Index(int id)
{
var model = _personService.GetActivePerson(id);
if(model == null)
return PersonNotFoundResult();
var viewModel = new VMPerson(model);
return View(viewModel);
}
}
现在我意识到我没有为PersonService创建一个接口并将其传递给我的控制器的构造函数。原因是1)我不打算模拟我的PersonService和2)我觉得我不需要注入我的依赖,因为我的PersonController现在只需要依赖一种PersonService。
我是单位测试的新手,我总是很高兴被证明我错了。请指出为什么我测试我的控制器的方式可能是一个非常糟糕的主意(除了我的测试运行时间明显增加)。
答案 0 :(得分:5)
嗯。这里有几件事情。
首先,看起来您正在尝试测试控制器方法。太棒了:))
所以这意味着,控制器需要的任何东西都应该被嘲笑。这是因为
好的,那么让我们来看看你做了什么,我会看看我是否可以重构它以使其更加可测试。
-REMEMBER-我正在测试 CONTROLLER METHOD ,而不是控制器方法调用/依赖的东西。
所以这意味着我不关心服务实例或存储库实例(您决定遵循的架构方式)。
注意:我保持简单,所以我已经删除了很多废话等等。
首先,我们需要一个存储库接口。这可以实现为内存中的repo,实体框架repo等。你很快就会明白为什么。
public interface ILESRepository
{
IQueryable<Person> GetAll();
}
在这里,我们使用界面。这意味着使用模拟IRepository
或真实实例非常简单和棒极了。
public class PersonController : Controller
{
private readonly ILESRepository _repository;
public PersonController(ILESRepository repository)
{
if (repository == null)
{
throw new ArgumentNullException("repository");
}
_repository = repository;
}
public ActionResult Index(int id)
{
var model = _repository.GetAll<Person>()
.FirstOrDefault(x => x.Id == id);
var viewModel = new VMPerson(model);
return View(viewModel);
}
}
好的 - 这是神奇的金钱拍摄的东西。
首先,我们创造了一些假人。在这里和我一起工作......我会告诉你我们在哪里使用它。这只是一个无聊,简单的POCO
列表。
public static class FakePeople()
{
public static IList<Person> GetSomeFakePeople()
{
return new List<Person>
{
new Person { Id = 1, Name = "John" },
new Person { Id = 2, Name = "Fred" },
new Person { Id = 3, Name = "Sally" },
}
}
}
现在我们有了测试本身。我正在使用xUnit
作为我的测试框架,使用moq
作为我的模拟。任何框架都可以,这里。
public class PersonControllerTests
{
[Fact]
public void GivenAListOfPeople_Index_Returns1Person()
{
// Arrange.
var mockRepository = new Mock<ILESRepository>();
mockRepository.Setup(x => x.GetAll<Person>())
.Returns(
FakePeople.GetSomeFakePeople()
.AsQueryable);
var controller = new PersonController(mockRepository);
controller.FakeOutContext();
// Act.
var result = controller.Index(person.Id) as ViewResult;
// Assert.
Assert.NotNull(result);
var model = result.Model as VMPerson;
Assert.NotNull(model);
Assert.Equal(1, model.Person.Id);
Assert.Equal("John", model.Person.Username);
// Make sure we actually called the GetAll<Person>() method on our mock.
mockRepository.Verify(x => x.GetAll<Person>(), Times.Once());
}
}
好的,让我们来看看我做了什么。
首先,我安排我的废话。我首先创建了ILESRepository
的模拟。
然后我说:如果有人曾调用GetAll<Person>()
方法,那么......不要 - 真正地命中数据库或文件或其他任何东西......只返回在FakePeople.GetSomeFakePeople()
中创建的人员列表
所以这就是控制器中会发生的......
var model = _repository.GetAll<Person>()
.FirstOrDefault(x => x.Id == id);
首先,我们要求mock使用GetAll<Person>()
方法。我只是'设置'来返回一个人的列表..所以我们有一个3 Person
个对象的列表。接下来,我们在3个FirstOrDefault(...)
对象的列表上调用Person
..它返回单个对象或null,具体取决于id
的值。
多田!这是钱拍的:)
现在回到单元测试的其余部分。
我们Act
然后我们Assert
。那里没什么难的。
对于奖励积分,我verify
我们实际上称为GetAll<Person>()
方法,在模拟器上...在控制器的Index
方法内。这是一个安全调用,以确保我们的控制器逻辑(我们正在测试)是正确的。
有时,您可能想检查错误的情况,就像传入错误数据的人一样。这意味着您可能永远不会使用模拟方法(这是正确的),因此您verify
从未调用它们。
好的 - 问题,上课?
答案 1 :(得分:2)
即使你不打算模拟一个接口,我强烈建议你不要通过在构造函数中创建对象来隐藏对象的真正依赖关系,你违反了单一责任原则而你正在编写不可测试的代码。
编写测试时最重要的考虑因素是:“编写测试没有神奇的钥匙”。有很多工具可以帮助你编写测试但是真正的努力应该放在编写可测试代码而不是试图 hack 我们现有的代码来编写测试这通常最终是集成测试,而不是单元测试。
在构造函数中创建一个新对象是代码无法测试的第一个重要信号之一。
当我开始编写测试时,这些链接帮助了我很多,让我告诉你,在你开始之后,这将成为你日常工作的一个自然部分,你会喜欢写测试的好处我可以没有想象自己在没有测试的情况下编写代码
清洁代码指南(在Google中使用):http://misko.hevery.com/code-reviewers-guide/
要获取更多信息,请阅读以下内容:
http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/
观看由Misko Hevery播放的视频
http://www.youtube.com/watch?v=wEhu57pih5w&feature=player_embedded
编辑:
Martin Fowler的这篇文章解释了Classic和Mockist TDD方法之间的区别
http://martinfowler.com/articles/mocksArentStubs.html
总结:
经典TDD方法:这意味着在不创建替代或双打(模拟,存根,虚拟变量)的情况下测试所有内容,但Web服务或数据库等外部服务除外。经典测试人员仅使用双打作为外部服务
Mockist TDD方法:遵循Mockist方法的人将孤立地测试所有代码,因为它们将为每个依赖创建双精度
这是关于编写可测试代码的另一篇很棒的文章
http://www.loosecouplings.com/2011/01/how-to-write-testable-code-overview.html
答案 2 :(得分:1)
有一些缺点。
首先,当您的测试依赖于外部组件(如实时数据库)时,该测试不再具有可预测性。它可能由于多种原因而失败 - 网络中断,数据库帐户上的密码更改,丢失一些DLL等。因此,当您的测试突然失败时,您无法立即确定缺陷 的位置。这是数据库问题吗?你班上有些棘手的错误?
如果您只需知道哪个测试失败就可以立即回答该问题,那么您将获得令人羡慕的defect localization质量。
其次,如果 是数据库问题,那么依赖它的所有测试都会立即失败。这可能不是那么严重,因为你可能已经意识到原因是什么,但我保证它会让你慢下来检查每一个。广泛的失败可以掩盖真正的问题,因为你不想在每次50次测试中查看异常。
我知道你想听听除了执行时间之外的其他因素,但这确实很重要。您希望尽可能频繁地运行测试,而较长的运行时则不鼓励这样做。
我有两个项目:一个有600多个测试,运行时间为10秒,一个有40多个测试,运行时间为50秒(这个项目实际上是故意与数据库对话)。我在开发过程中更频繁地运行更快的测试套件很多。猜猜哪一个我觉得更容易合作?
所有这些都表明,测试外部组件是有价值的。只是当你进行单元测试时。集成测试更脆弱,速度更慢。这使他们更加昂贵。
答案 3 :(得分:1)
在单元测试中访问数据库会产生以下后果:
您必须询问测试业务规则是否值得。在大多数情况下,答案可能是“不”。
我遵循的方法是: