C# 单元测试,模拟-简单案例:服务-存储库
考虑以下服务块:C# 单元测试,模拟-简单案例:服务-存储库,c#,unit-testing,service,mocking,repository,C#,Unit Testing,Service,Mocking,Repository,考虑以下服务块: public class ProductService : IProductService { private IProductRepository _productRepository; // Some initlization stuff public Product GetProduct(int id) { try { return _productRepository.GetProduct(id); }
public class ProductService : IProductService {
private IProductRepository _productRepository;
// Some initlization stuff
public Product GetProduct(int id) {
try {
return _productRepository.GetProduct(id);
} catch (Exception e) {
// log, wrap then throw
}
}
}
让我们考虑一个简单的单元测试:
[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
var product = EntityGenerator.Product();
_productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);
Product returnedProduct = _productService.GetProduct(product.Id);
Assert.AreEqual(product, returnedProduct);
_productRepositoryMock.VerifyAll();
}
起初,这个测试似乎还可以。但是让我们稍微改变一下我们的服务方式:
public Product GetProduct(int id) {
try {
var product = _productRepository.GetProduct(id);
product.Owner = "totallyDifferentOwner";
return product;
} catch (Exception e) {
// log, wrap then throw
}
}
如何重写一个给定的测试,它通过了第一个服务方法,而失败了第二个服务方法
您如何处理这种简单的场景
提示1:给定的测试是不好的,因为产品和返回的产品实际上是相同的参考
提示2:实现相等成员(object.equals)不是解决方案
提示3:目前,我使用AutoMapper创建产品实例(expectedProduct)的克隆,但我不喜欢这个解决方案
提示4:我不是在测试SUT是否没有做某事。我正在尝试测试SUT是否返回了与从存储库返回的对象相同的对象。
产品
网关中进行更改来更改产品的所有者。您可以在模型中进行更改
但如果你坚持,那就听你的测试。他们告诉您,您有可能从拥有不正确所有者的网关中提取产品。哎呀,看起来像是一条商业规则。应在模型中测试
你也可以使用mock。为什么要测试实现细节?网关只关心\u productRepository.GetProduct(id)
返回产品。不是产品本身
如果您以这种方式进行测试,您将创建脆弱的测试。如果产品进一步改变怎么办。现在到处都是不合格的测试
您的产品(型号)消费者是唯一关心产品实施的人
因此,网关测试应如下所示:
[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
var product = EntityGenerator.Product();
_productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);
_productService.GetProduct(product.Id);
_productRepositoryMock.VerifyAll();
}
[Test]
public void GetProduct_GetsProductFromRepository()
{
var product = EntityGenerator.Product();
_productRepositoryMock
.Setup(pr => pr.GetProduct(product.Id))
.Returns(product);
Product returnedProduct = _productService.GetProduct(product.Id);
Assert.AreSame(product, returnedProduct);
}
不要把业务逻辑放在它不属于的地方!它的推论是不要在没有业务逻辑的情况下测试业务逻辑。就个人而言,我不在乎这一点。测试应该确保代码执行了您想要的操作很难测试代码没有做什么,在这种情况下,我不想麻烦 测试实际上应该如下所示:
[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
var product = EntityGenerator.Product();
_productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);
_productService.GetProduct(product.Id);
_productRepositoryMock.VerifyAll();
}
[Test]
public void GetProduct_GetsProductFromRepository()
{
var product = EntityGenerator.Product();
_productRepositoryMock
.Setup(pr => pr.GetProduct(product.Id))
.Returns(product);
Product returnedProduct = _productService.GetProduct(product.Id);
Assert.AreSame(product, returnedProduct);
}
我的意思是,这是您正在测试的一行代码。好吧,一种方法是传递产品的模拟,而不是实际的产品。通过严格控制产品,确保不影响产品。(我假设您正在使用最小起订量,看起来您正在使用)
[测试]
public void GetProduct\u return\u与\u productRepository()上的\u GetProduct\u相同的\u产品\u{
var product=新模拟(MockBehavior.Strict);
_productRepositoryMock.Setup(pr=>pr.GetProduct(product.Id)).Returns(product);
返回的产品Product=\u productService.GetProduct(Product.Id);
断言.AreEqual(产品、退货产品);
_productRepositoryMock.VerifyAll();
product.VerifyAll();
}
也就是说,我不确定你是否应该这样做。这个测试做的太多了,可能表明在某个地方还有另一个需求。找到需求并创建第二个测试。也许你只是想阻止自己做一些愚蠢的事情。我不这么认为,因为你可以做很多愚蠢的事情。尝试测试每种方法都会花费太长的时间。我不确定单元测试是否应该关注“给定的方法做了什么而不是”。有无数的步骤是可能的。严格来说,测试“GetProduct(id)返回与productRepository上的GetProduct(id)相同的产品”是正确的,无论有无行
product.Owner=“TotallyDifferentTowner”
但是,您可以创建一个测试(如果需要)“GetProduct(id)return product,其内容与productRepository上的GetProduct(id)相同”,在该测试中,您可以创建一个产品实例的(可能很深)克隆,然后您应该比较两个对象的内容(因此没有object.Equals或object.ReferenceEquals)
单元测试不能保证100%无错误且行为正确。单元测试的一种思维方式是编码规范。当您使用
EntityGenerator
为测试和实际服务生成实例时,可以看到您的测试表达了需求
- 该服务使用EntityGenerator生成产品实例
- 该服务使用EntityGenerator生成无法修改的产品实例
var product = EntityGenerator.Product();
// [ Change ]
var originalOwner = product.Owner;
// assuming owner is an immutable value object, like String
// [...] - record other properties as well.
Product returnedProduct = _productService.GetProduct(product.Id);
Assert.AreEqual(product, returnedProduct);
// [ Change ] verify the product is equivalent to the original spec
Assert.AreEqual(originalOwner, returnedProduct.Owner);
// [...] - test other properties as well
(更改是我们从新创建的产品中检索所有者,并从服务返回的产品中检查所有者。)
这体现了一个事实,即所有者和其他产品属性必须等于生成器的原始值。这可能看起来像是我在说显而易见的事情,因为代码非常琐碎,但如果您从需求规范的角度考虑,它运行得相当深入
我经常通过规定“如果我更改这行代码,调整一两个关键常量,或者注入一些代码错误(例如,将!=更改为==),哪个测试将捕获错误?”来“测试我的测试”,这样做可以真正发现是否有捕获问题的测试。有时不是,在这种情况下,是时候看看测试中隐含的需求了,看看我们如何收紧它们。在没有实际需求捕获/分析的项目中,这是一个有用的工具,可以强化测试,以便在发生意外更改时失败
当然,你必须务实。你不能合理地期望处理所有的变化——有些变化简直是荒谬的,而且
Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg.Is.Anything);
Dep<IProduct>().AssertNoPropertyOrMethodWasCalled()
[Specification]
public class When_product_service_has_get_product_called_with_any_id
: ProductServiceSpecification
{
private int _productId;
private IProduct _actualProduct;
[It]
public void Should_return_the_expected_product()
{
this._actualProduct.Should().Be.EqualTo(Dep<IProduct>());
}
[It]
public void Should_not_have_the_product_modified()
{
Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg<string>.Is.Anything);
// or write your own extension method:
// Dep<IProduct>().AssertNoPropertyOrMethodWasCalled();
}
public override void GivenThat()
{
var randomGenerator = new RandomGenerator();
this._productId = randomGenerator.Generate<int>();
Stub<IProductRepository, IProduct>(r => r.GetProduct(this._productId));
}
public override void WhenIRun()
{
this._actualProduct = Sut.GetProduct(this._productId);
}
}
// If you're not a purist, go ahead and verify all the attributes in a single
// test - Get_Product_Does_Not_Modify_The_Product_Returned_By_The_Repository
[Test]
public Get_Product_Does_Not_Modify_Owner() {
Product mockProduct = mockery.NewMock<Product>(MockStyle.Transparent);
Stub.On(_productRepositoryMock)
.Method("GetProduct")
.Will(Return.Value(mockProduct);
Expect.Never
.On(mockProduct)
.SetProperty("Owner");
_productService.GetProduct(0);
mockery.VerifyAllExpectationsHaveBeenMet();
}
public class Assert2
{
public static void IsSameValue(object expectedValue, object actualValue) {
JavaScriptSerializer serializer = new JavaScriptSerializer();
var expectedJSON = serializer.Serialize(expectedValue);
var actualJSON = serializer.Serialize(actualValue);
Assert.AreEqual(expectedJSON, actualJSON);
}
}
public static class It2
{
public static T IsSameSerialized<T>(T expectedRecord) {
JavaScriptSerializer serializer = new JavaScriptSerializer();
string expectedJSON = serializer.Serialize(expectedRecord);
return Match<T>.Create(delegate(T actual) {
string actualJSON = serializer.Serialize(actual);
return expectedJSON == actualJSON;
});
}
}