我们发现自己在许多测试用例中编写重复的fixture / mock设置 - 就像这样:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var encodingMock = fixture.Freeze<Mock<IEncodingWrapper>>();
var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();
var httpResponseMock = fixture.Freeze<Mock<IHttpWebResponseWrapper>>();
var httpHeaderMock = fixture.Freeze<Mock<IHttpHeaderCollectionWrapper>>();
var etag = fixture.CreateAnonymous<string>();
byte[] data = fixture.CreateAnonymous<byte[]>();
Stream stream = new MemoryStream(data);
encodingMock.Setup(m => m.GetBytes(It.IsAny<string>())).Returns(data);
httpHeaderMock.SetupGet(m => m[It.IsAny<string>()]).Returns(etag).Verifiable();
httpClientMock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);
httpResponseMock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);
httpResponseMock.SetupGet(m => m.Headers).Returns(httpHeaderMock.Object);
httpResponseMock.Setup(m => m.GetResponseStream()).Returns(stream);
根据测试应该是自包含的并且从头到尾可读的想法,我们不使用神奇的Setup / Teardown方法。
我们能以任何方式(AutoFixture自定义,辅助方法)减少这些测试的“笨拙工作”吗?
答案 0 :(得分:15)
来自Growing Object-Oriented Software(GOOS)提出了一个很好的建议:如果测试很难写,那就是关于被测系统(SUT)的API的反馈。考虑重新设计SUT。在这个特定的例子中,看起来SUT至少有四个依赖关系,这可能表明违反了Single Responsibility Principle。可以refactor to Facade Services吗?
GOOS的另一个重要建议是
在上面的示例中,您似乎需要为真正查询的方法执行大量Moq设置。这也表明了测试气味。某处有Law of Demeter违规吗?是否有可能削减方法链?
答案 1 :(得分:11)
您可以创建一个复合自定义,它将使用所有包含的自定义来自定义夹具。
public class HttpMocksCustomization : CompositeCustomization
{
public HttpMocksCustomization()
: base(
new AutoMoqCustomization(),
new HttpWebClientWrapperMockCustomization(),
new HttpWebResponseWrapperMockCustomization()
// ...
)
{
}
}
每个自定义都可以定义如下:
public class HttpWebClientWrapperMockCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
var mock = new Mock<IHttpWebClientWrapper>();
mock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);
fixture.Inject(mock);
}
}
public class HttpWebResponseWrapperMockCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
var mock = new Mock<IHttpWebResponseWrapper>();
mock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);
fixture.Inject(mock);
}
}
// The rest of the Customizations.
然后在测试方法中你可以这样做:
var fixture = new Fixture().Customize(new HttpMocksCustomization());
这样,当您请求Mock实例时,您不必重复设置步骤。我们之前定制的那个将被退回:
var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();
但是,如果您使用xUnit.net ,事情可以进一步简化。
您可以创建一个AutoDataAttribute派生类型,以提供由AutoFixture生成的自动生成的数据样本作为xUnit.net的Theory属性的扩展:
public class AutoHttpMocksDataAttribute : AutoDataAttribute
{
public AutoHttpMocksDataAttribute()
: base(new Fixture().Customize(new HttpMocksCustomization()))
{
}
}
然后,在您的测试方法中,您可以将Mocks作为参数传递:
[Theory, AutoHttpMocksData]
public void MyTestMethod([Freeze]Mock<IHttpWebClientWrapper> httpClientMock, [Freeze]Mock<IHttpWebResponseWrapper> httpResponseMock)
{
// ...
}
答案 2 :(得分:4)
如果您的所有测试都使用此代码,则应将其置于设置/拆除方法中。如果您的设置/拆除方法有些复杂,只要您的所有单元测试都依赖于它,这是可以的。这肯定比复制每个测试中所有复杂的东西更好。当我阅读测试时,我知道设置和拆卸是每个测试的一部分,因此我不认为你在可读性方面也失去了任何东西。要避免的是在设置中包含并非每个测试都需要的东西。这会产生令人困惑的情况,即您的安装方法与您的所有测试都不匹配。理想情况下,您的设置方法应该100%应用于每个测试。
如果共享代码未在所有测试中使用,请将共享代码提取到辅助函数中。编写好的测试代码就像编写任何其他优秀代码一样,并且适用相同的原则。