我正在阅读大量文档和示例,了解如何正确地对标题中的三个组件进行单元测试。我想出了一种针对我的业务逻辑的方法的测试方法,但它感觉非常笨重和肮脏。
我希望从这个主题更有经验的人那里获得一些反馈,看看我如何改进它。
以下是代码,解释如下:
[Fact]
public void ShouldGetItemWithSameId()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var facade = fixture.Freeze<Mock<IDataFacade>>();
facade.Setup(c => c.Get(It.IsAny<int>())).Returns((int i) => new Item { Key = i });
var sut = fixture.Create<BusinessLogic>();
var expected = fixture.Create<int>();
Assert.Equal(expected, sut.Get(expected).Key);
}
我的BusinessLogic
类使用IDataFacade
作为构造函数参数,该参数在其Get(int)
方法中负责检索具有相同Id的项目,非常基本的东西。
我冻结IDataFacade
模拟器并将其设置为构造与It.IsAny<int>
中的Id匹配的对象。然后我创建我的SUT并测试它。工作正常。
我想了解以下是否可以改进:
Query
方法,它接受一个包含许多属性的类,这些属性将用作匹配被查询类型的属性的过滤器。在这种情况下,我不知道如何正确地进行&#34;设置&#34;模拟的一部分,因为我必须初始化所有或接近所有返回类型的属性,在这种情况下,它不是单个项目而是整个集合我使用Theory
使用AutoMoqData
进行了一些其他测试,但我无法使用该方法实现此测试(我认为更复杂的测试),因此我切换回普通{{1手动实例化夹具。
非常感谢任何帮助。
答案 0 :(得分:8)
总体而言,原始测试看起来不错。以通用的方式从测试中提取Stubs and Mocks的设置是不可能的,也不容易。
您可以做的事情是尽量减少测试的排列阶段。这是使用AutoFixture.Xunit自己的单元测试DSL重新编写的原始测试:
[Theory, TestConventions]
public void ShouldGetItemWithSameId(
[Frozen]Mock<IDataFacade> facadeStub,
BusinessLogic sut,
int expected)
{
facadeStub
.Setup(c => c.Get(It.IsAny<int>()))
.Returns((int i) => new Item { Key = i });
var result = sut.Get(expected);
var actual = result.Key;
Assert.Equal(expected, actual);
}
TestConventions
属性定义为:
public class TestConventionsAttribute : AutoDataAttribute
{
public TestConventionsAttribute()
: base(new Fixture().Customize(new AutoMoqCustomization()))
{
}
}
HTH
示例中使用的示例类型:
public class Item
{
public int Key { get; set; }
}
public interface IDataFacade
{
Item Get(int p);
}
public class BusinessLogic
{
private readonly IDataFacade facade;
public BusinessLogic(IDataFacade facade)
{
this.facade = facade;
}
public Item Get(int p)
{
return this.facade.Get(p);
}
}
答案 1 :(得分:5)
你的测试对我来说很好,虽然我会推荐一个改变。如果传递了预期值,则可以收紧以下行以仅返回预期值:
facade.Setup(c => c.Get(It.IsAny<int>())).Returns((int i) => new Item { Key = i });
所有你需要做的就是移动预期变量并像这样更改Is.IsAny:
var expected = fixture.Create<int>();
facade.Setup(c => c.Get(expected)).Returns((int i) => new Item { Key = i });
我必须测试更复杂的方法,比如一个Query方法,它接受一个包含很多属性的类,这些属性将用作匹配被查询类型的属性的过滤器。在这种情况下,我不知道如何正确地执行模拟的“设置”部分,因为我必须初始化所有或接近所有返回类型的属性,并且在这种情况下它不是单个项目但是整个系列
我认为您不需要初始化返回类型的所有值。我猜你的DataFacade返回一个对象(或在这种情况下的列表)?您需要做的就是确保返回的对象与DataFacade返回的对象的引用相匹配,您不需要担心属性等,因为您没有测试这些对象的构造,只是它们是回。如果我误解了你并且你正在构建BusinessLogic中的对象,那么这是另一回事。就个人而言,我不会将业务逻辑依赖于数据层,但这是一个不同的讨论。 : - )
设置部分感觉不合适,我希望能够在更多方法中重复使用
你可以。将其提取到单独的方法中,或者,如果它适用于类中的每个测试,则将其放入设置方法中。我不熟悉XUnit,但我使用的每个其他测试框架都提供了进行常规设置的能力,所以我怀疑XUnit会有什么不同。
我最后的评论,对待你的测试代码,就像对待你的生产代码一样,如果它看起来很乱,那就做一些让它变得更好的东西。测试非常适合描述系统的行为,但如果它们难以阅读(和维护),则会失去很多价值。
编辑,结果证明这不是我最后的评论!如果你是TDD的新手,我不确定你是不是,不要陷入测试应用程序中每个类的陷阱,这是一种普遍的模式已经变得普遍并且在我看来它贬值了TDD 。我写了一篇blog post on my feelings,Ian Cooper就这件事给了superb presentation。
答案 2 :(得分:2)
一些基础知识:
在运行每个单独的Test之前,会对Test类进行实例化(并调用其构造函数)。例如如果你的Test类有三个带[Fact]属性的方法,它会被实例化三次
TestFixture类是另一个类,它意味着为测试类中的所有测试设置单次。
要使这项工作,您的测试类必须实现IUseFixture接口,例如实现一个成员SetFixture()
您可以为多个测试类使用相同的MyTestFixture类。
在TestFixture内部,您可以执行所有模拟设置。
这里的总体布局:
public class MyTestFixture
{
public Mock<MyManager> ManagerMock;
public TestFixture() // runs once
{
ManagerMock.Setup(...);
}
}
public MyTestClass : IUseFixture<MyTestFixture>
{
private MyTestFixture fixture;
public MyTestClass()
{
// ctor runs for each [Fact]
}
public void SetFixture(MyTestFixture fixture)
{
this.fixture = fixture;
}
[Fact]
public void MyTest
{
// use Mock
fixture.ManagerMock.DoSomething()
}
}