我是单元测试的新手,感觉好像我在这里错过了一些非常重要的事情。我想在下面测试DoSomethingWithArray的结果:
class Traffic:ITraffic
{
private HugeArray _hugeArray;
public Traffic(HugeArray hugeArray)
{
_hugeArray = hugeArray;
}
public int DoSomethingWithArray()
{
var ret = 0;
//Here some code that does something with big array and stores some integer values in ret
return ret;
}
}
class HugeArray
{
//This is my custom data structure;
}
interface ITraffic
{
int DoSomethingWithArray();
}
我正在使用Nunit,从我阅读的内容来看,模拟接口比模拟类要好。我的问题是我想测试的是Traffic类中DosomethingWithArray的CONCRETE功能,我很难概念化ITraffic的接口。 我在这里想念什么?
编辑,这是我测试班级的方式
[TestFixture]
public class TrafficTests
{
private Traffic _traffic;
private const int size = 1000000;
private const int key = 1851925790;
[OneTimeSetUp]
public void Setup()
{
var hugeArray = new HugeArray(size);
//Some Setups to create an edge case, not relevant to my question
hugeArray.AddValue(size - 1, Int.MaxValue);
hugeArray.AddValue(size - 2, key);
//This is the object I want to test,
_traffic = new Traffic(hugeArray);
}
[Test]
public void DoSomethingWithArray_Test()
{
Assert.DoesNotThrow(() =>
{
var ret = _traffic.DoSomethingWithArray();
Assert.AreEqual(ret, 233398);
});
}
}
我的问题是:这种方法看起来正确吗?是为测试创建的对象还可以,还是应该模拟ITraffic接口呢?
答案 0 :(得分:3)
在您的示例中,您正在测试Traffic
的公共方法。 Traffic
实现ITraffic
无关紧要。如果您从类中删除了: ITraffic
,使其不再实现该接口,那么它将根本不会改变您测试Traffic
的方式。
您正在测试Traffic
。我们不会嘲笑我们正在测试的东西。我们嘲笑我们正在不测试的东西。
假设我有一个可验证地址的类:
public class AddressValidator
{
public ValidationResult ValidateAddress(Address address)
{
var result = new ValidationResult();
if(string.IsNullOrEmpty(address.Line1))
result.AddError("Address line 1 is empty.");
if(string.IsNullOrEmpty(address.City))
result.AddError("The city is empty.");
// more validations
return result;
}
}
此类是否实现接口都没有关系。如果我正在测试该类,那么没有什么可模仿的。
假设我意识到我还需要验证邮政编码,但是为此,我可能需要查询一些外部数据以查看城市是否与邮政编码匹配。也许不同的国家有所不同。因此,我编写了一个新接口并将其注入到此类中:
public interface IPostalCodeValidator
{
ValidationResult ValidatePostalCode(Address address);
}
public class AddressValidator
{
private readonly IPostalCodeValidator _postalCodeValidator;
public AddressValidator(IPostalCodeValidator postalCodeValidator)
{
_postalCodeValidator = postalCodeValidator;
}
public ValidationResult ValidateAddress(Address address)
{
var result = new ValidationResult();
if (string.IsNullOrEmpty(address.Line1))
result.AddError("Address line 1 is empty.");
if (string.IsNullOrEmpty(address.City))
result.AddError("The city is empty.");
var postalCodeValidation = _postalCodeValidator.ValidatePostalCode(address);
if (postalCodeValidation.HasErrors)
result.AddErrors(postalCodeValidation.Errors);
return result;
}
}
邮政编码验证非常复杂,以至于要在自己的类中进行自己的测试。当我们测试AddressValidator
时,我们不想要测试邮政编码验证器。我们只想单独测试该类,然后分别测试另一个类。在AddressValidator
中,要确保调用_postalCodeValidator.ValidatePostalCode
,并且如果返回错误,则将它们添加到验证结果中。
我们此处不是不是测试IPostalCodeValidator
(或其实现),因此我们对其进行了模拟。例如,使用Moq:
public void AddressValidator_adds_postal_code_errors()
{
var postalCodeError = new ValidationResult();
postalCodeError.AddError("Bad!");
postalCodeError.AddError("Worse!");
var postalCodeValidatorMock = new Mock<IPostalCodeValidator>();
postalCodeValidatorMock.Setup(x => x.ValidatePostalCode(It.IsAny<Address>()))
.Returns(postalCodeError);
var subject = new AddressValidator(postalCodeValidatorMock.Object);
var result = subject.ValidateAddress(new Address());
Assert.IsTrue(result.Errors.Contains("Bad!"));
Assert.IsTrue(result.Errors.Contains("Worse!"));
}
我们实际上并没有验证邮政编码。我们只是说,为了进行测试,邮政编码验证器总是会返回这两个错误。然后,我们确保AddressValidator
会调用它并按照我们期望的方式处理这些错误。
这基本上就是模拟。这是对某些简单操作的虚假实现,例如罐头响应,因此我们可以确保以预期的方式处理罐头响应。如果AddressValidator
正确处理了结果,则它可以正常工作。完成了。
为确保 real 邮政编码验证器返回正确的结果,我们可以为该类编写测试。这样,每个类都会做一些简单的事情,并进行测试以确保其正确执行了它的工作。当我们将它们放在一起时,整个事情很有可能会起作用。如果我们中断IPostalCodeValidator
的实现,则该类的测试将失败,但AddressValidator
的测试仍将通过。这样一来,我们就可以快速了解哪个部分已损坏,因为它们都经过了单独的测试,因此我们不必运行和调试大量代码即可找出问题所在。