我基本上都在努力教自己如何编码,我想遵循良好的做法.单元测试有明显的好处.在单元测试方面也有很多狂热,我更喜欢更实用的编码和生活方法.作为上下文,我正在编写我的第一个"真实"应用程序,它是使用asp.net MVC的无处不在的博客引擎.我通过自己的调整松散地遵循MVC店面架构.因此,这是我第一次真正涉足嘲弄对象.我将把代码示例放在问题的最后.
我很感激任何洞察力或外部资源,我可以用它来增加我对测试和模拟基础的理解.我在网上找到的资源通常是针对模拟的"方式",我需要更多地了解模拟的地点,原因和时间.如果这不是提出这个问题的最佳地点,请指出一个更好的地方.
我试图理解我从以下测试中获得的价值.UserService依赖于IUserRepository.服务层的价值是将逻辑与数据存储分开,但在这种情况下,大多数UserService调用只是直接传递给IUserRepository.没有太多实际逻辑需要测试的事实也可能是我担忧的根源.我有以下问题.
感觉就像代码只是在测试模拟框架是否正常工作.
为了模拟依赖关系,它使我的测试对IUserRepository实现有太多的了解.这是一个必要的邪恶吗?
我从这些测试中获得了什么价值?被测服务的简单性是否让我怀疑这些测试的价值.
我正在使用NUnit和Rhino.Mocks,但我应该很清楚我要完成的事情.
[SetUp] public void Setup() { userRepo = MockRepository.GenerateMock(); userSvc = new UserService(userRepo); theUser = new User { ID = null, UserName = "http://joe.myopenid.com", EmailAddress = "joe@joeblow.com", DisplayName = "Joe Blow", Website = "http://joeblow.com" }; } [Test] public void UserService_can_create_a_new_user() { // Arrange userRepo.Expect(repo => repo.CreateUser(theUser)).Return(true); // Act bool result = userSvc.CreateUser(theUser); // Assert userRepo.VerifyAllExpectations(); Assert.That(result, Is.True, "UserService.CreateUser(user) failed when it should have succeeded"); } [Test] public void UserService_can_not_create_an_existing_user() { // Arrange userRepo.Stub(repo => repo.IsExistingUser(theUser)).Return(true); userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false); // Act bool result = userSvc.CreateUser(theUser); // Assert userRepo.VerifyAllExpectations(); Assert.That(result, Is.False, "UserService.CreateUser() allowed multiple copies of same user to be created"); }
Doug R.. 19
基本上你在这里测试的是方法被调用,而不是它们是否实际工作.模拟应该做什么.他们只是检查方法是否被调用,并返回Return()语句中的任何内容,而不是调用方法.所以在你的断言中:
Assert.That(result, Is.False, "error message here");
这个断言总是会成功,因为你的期望总是会返回false,因为Return语句:
userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
我猜这在这种情况下不是那么有用.
模拟是有用的,例如,当你想要在代码中的某个地方进行数据库调用,但你不想实际调用数据库.你想假装数据库被调用,但你想设置一些假数据让它返回,然后(这里是重要的部分)测试用你的模拟返回的假数据做某事的逻辑.在上面的例子中,您省略了最后一步.想象一下,你有一个方法向用户显示一条消息,说明是否创建了新用户:
public string displayMessage(bool userWasCreated) { if (userWasCreated) return "User created successfully!"; return "User already exists"; }
那么你的测试就是
userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false); Assert.AreEqual("User already exists", displayMessage(userSvc.CreateUser(theUser)))
现在这有一些价值,因为你正在测试一些实际的行为.当然,您也可以直接通过传入"true"或"false"来测试它.你甚至不需要模拟那个测试.测试期望很好,但是我已经写了很多这样的测试,并得出了你所达到的相同结论 - 它只是没有那么有用.
简而言之,当您想抽象出外部因素(如数据库或Web服务调用等)并在此时注入已知值时,模拟很有用.但直接测试模拟通常并不常见.
基本上你在这里测试的是方法被调用,而不是它们是否实际工作.模拟应该做什么.他们只是检查方法是否被调用,并返回Return()语句中的任何内容,而不是调用方法.所以在你的断言中:
Assert.That(result, Is.False, "error message here");
这个断言总是会成功,因为你的期望总是会返回false,因为Return语句:
userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
我猜这在这种情况下不是那么有用.
模拟是有用的,例如,当你想要在代码中的某个地方进行数据库调用,但你不想实际调用数据库.你想假装数据库被调用,但你想设置一些假数据让它返回,然后(这里是重要的部分)测试用你的模拟返回的假数据做某事的逻辑.在上面的例子中,您省略了最后一步.想象一下,你有一个方法向用户显示一条消息,说明是否创建了新用户:
public string displayMessage(bool userWasCreated) { if (userWasCreated) return "User created successfully!"; return "User already exists"; }
那么你的测试就是
userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false); Assert.AreEqual("User already exists", displayMessage(userSvc.CreateUser(theUser)))
现在这有一些价值,因为你正在测试一些实际的行为.当然,您也可以直接通过传入"true"或"false"来测试它.你甚至不需要模拟那个测试.测试期望很好,但是我已经写了很多这样的测试,并得出了你所达到的相同结论 - 它只是没有那么有用.
简而言之,当您想抽象出外部因素(如数据库或Web服务调用等)并在此时注入已知值时,模拟很有用.但直接测试模拟通常并不常见.
你是对的:服务的简单性使得这些测试无趣.直到您在服务中获得更多业务逻辑,您才能从测试中获得价值.
你可能会考虑这样的一些测试:
CreateUser_fails_if_email_is_invalid() CreateUser_fails_if_username_is_empty()
另一个评论:它看起来像一个代码味道,你的方法返回布尔值来表示成功或失败.您可能有充分的理由这样做,但通常您应该将异常传播出去.这也使得编写好的测试变得更加困难,因为你在检测你的方法是否因"正当理由"而失败时会遇到问题,你可以写一下CreateUser_fails_if_email_is_invalid() - 测试如下:
[Test] public void CreateUser_fails_if_email_is_invalid() { bool result = userSvc.CreateUser(userWithInvalidEmailAddress); Assert.That(result, Is.False); }
它可能适用于您现有的代码.使用TDD Red-Green-Refactor-cycle可以缓解这个问题,但是由于电子邮件无效而能够实际检测到方法失败甚至更好,而不是因为另一个问题.
如果您在编写代码之前编写测试,则可以从单元测试中获得更多.感觉您的测试不值得的原因之一是您没有获得让您的测试推动设计的价值.之后编写测试通常只是一项练习,看看你是否能记住可能出错的一切.首先编写测试会让您考虑如何实际实现该功能.
这些测试并不是那么有趣,因为正在实现的功能非常基础.你进行模拟的方式似乎很标准 - 模拟被测试的类所依赖的东西,而不是被测试的类.可测试性(或良好的设计意识)已经促使您实现接口并使用依赖注入来减少耦合.您可能想要考虑更改错误处理,正如其他人所建议的那样.很高兴知道为什么,如果只是为了提高测试质量,例如,CreateUser失败了.您可以使用异常或out
参数(如果我没记错的话,这是MembershipProvider的工作方式).