这个问题涉及一般的单元测试技术,可能非常有用的广泛适用场景。但通过一个例子更容易理解,以更好地说明我的问题。
假设我想测试覆盖Equals()
的所有类型都是正确的。由于Equals()
在System.Object
中被定义为虚拟,因此各种类型都可能会改变这种行为。执行此操作的每种类型都必须进行测试,以确保新行为遵循该方法调用者的隐含期望。特别是对于Equals()
,如果重写该方法,新实现必须确保两个相等的对象也具有相同的哈希码,如System.Object.GetHashCode()
所定义。
因此,要强制执行此操作,将需要多个测试类,并且它们都将测试所有这些类型的相同行为一致性。
为了避免重新输入测试这种类型所需的所有TestMethod,我改为定义一个如下所示的基本测试类,让这些测试类都继承相同的行为测试套件:
/// <summary>
/// Test fixture base class for testing types that overrides Object.Equals()
/// </summary>
/// <typeparam name="T">The production type under test</typeparam>
public abstract class EqualsFixtureBase<T>
{
#region Equals tests
protected static void CompareInstances(T inst1, T inst2, bool expectedEquals)
{
Assert.AreEqual(expectedEquals, inst1.Equals((T)inst2));
Assert.AreEqual(expectedEquals, inst1.Equals((object)inst2));
if (expectedEquals)
{
// equal instances MUST have identical hash codes
// this is a part of the .NET Equals contract
Assert.AreEqual(inst1.GetHashCode(), inst2.GetHashCode());
}
else
{
if (inst2 != null)
{
Assert.AreNotEqual(inst1.GetHashCode(), inst2.GetHashCode());
}
}
}
/// <summary>
/// Creates version 1 instance of the type under test, not 'Equal' to instance 2.
/// </summary>
/// <returns>An instance created with properties 1.</returns>
protected abstract T CreateInstance1();
/// <summary>
/// Creates version 2 instance of the type under test, not 'Equal' to instance 1.
/// </summary>
/// <returns>An instance created with properties 2.</returns>
protected abstract T CreateInstance2();
/// <summary>
/// Creates an instance equal to the version 1 instance, but not the identical
/// same object.
/// </summary>
/// <returns>An instance created with properties equal to instance 1.</returns>
protected abstract T CreateInstanceThatEqualsInstance1();
[TestMethod]
public void Equals_NullOrDefaultValueTypeInstance()
{
T instance = CreateInstance1();
CompareInstances(instance, default(T), false);
}
[TestMethod]
public void Equals_InstanceOfAnotherType()
{
T instance = CreateInstance1();
Assert.IsFalse(instance.Equals(new object()));
}
[TestMethod]
public void Equals_SameInstance()
{
T slot1 = CreateInstance1();
CompareInstances(slot1, slot1, true);
}
[TestMethod]
public void Equals_EqualInstances()
{
T slot1 = CreateInstance1();
T slot2 = CreateInstanceThatEqualsInstance1();
CompareInstances(slot1, slot2, true);
CompareInstances(slot2, slot1, true);
}
[TestMethod]
public void Equals_NonEqualInstances()
{
T slot1 = CreateInstance1();
T slot2 = CreateInstance2();
CompareInstances(slot1, slot2, false);
CompareInstances(slot2, slot1, false);
}
#endregion Equals tests
}
然后,我可以为重写Equals()的每种类型重用这些TestMethod。例如,这将是用于测试System.String
类型正确实现Equals()
的测试类定义。
[TestClass]
public class ExampleOfAnEqualsTestFixture : EqualsFixtureBase<string>
{
[TestMethod]
public void Foo()
{
Assert.IsTrue(true);
}
protected override string CreateInstance1()
{
return "FirstString";
}
protected override string CreateInstance2()
{
return "SecondString";
}
protected override string CreateInstanceThatEqualsInstance1()
{
return "FirstString";
}
}
这也可以进一步扩展。例如,对于重载==和!=运算符的类型,可以定义第二个抽象测试基类(即EqualsOperatorsFixtureBase<T> : EqualsFixtureBase<T>
),它测试这些运算符的实现不仅正确,而且与Equals()
和GetHashCode()
的扩展定义。
我可以使用NUnit来做到这一点,但是在使用MsTest时我会遇到问题。
a)Visual Studio 2010仅发现Foo()
测试方法,而不是继承的测试方法,因此无法运行它们。似乎Visual Studio测试加载器不会遍历测试类的继承层次结构。
b)当我在TFS中检入这些类型时,TFS会找到抽象的EqualsFixtureBase类型并认为它是要运行的测试类。但由于它无法创建,因此无法运行它并将该类型的测试标记为不确定 - 这使测试运行失败,从而导致构建(!)失败。
有没有办法绕过这个,或者这是MsTest和Visual Studio的限制?
如果是这样,那么在VS / TFS的路线图中修复它
这非常有用,特别是在测试实现接口的生产类型时,或者是继承层次结构的一部分,其中某些成员具有语义“契约类型”属性或不变量 - 如果这是有意义的。
基本上,没有对此的支持会阻止我重构我的测试代码以删除重复。
由于
编辑:我发现this link到其中一个MSDN博客,它说如下“在Whidbey中,缺少对测试类继承的支持。在Nunit中,它完全受到支持。这将在Orcas中得到纠正。”
这是三年前写的。为什么还没有添加?我没有得到它,有合理的理由有这个,在我看来这将是一个小的改变。或者我只是没有在这里跳出正确的箍?
答案 0 :(得分:22)
使用VS 2010我没有看到与您相同的行为。当我将2个类复制到测试项目并编译它时,我得到了输出:
UTA004: Illegal use of attribute...The TestMethodAttribute can be
defined only inside a class marked with the TestClass attribute
所以我标记了 EqualsFixutureBase :
[TestClass]
public abstract class EqualsFixtureBase<T>
{
...
}
现在它在没有警告的情况下进行编译,当我为 ExampleOfAnEqualsTestFixture 选择运行测试时,它运行Foo并且所有5个继承的等于测试。此外,当我复制 ExampleOfAnEqualsTestFixture 并将其用于 int 并运行解决方案的测试时,我看到所有5个继承的测试运行(并传递)示例字符串类和示例int类。
除了您的示例之外,您是否正在做一些可能导致问题的事情?
答案 1 :(得分:10)
TestClassAttribute允许您将方法放在抽象基础中。 IgnoreAttribute从测试列表中排除基类。如果没有基数内的IgnoreAttribute属性,则基类和标有TestClassAttribute的子类都会执行。
[TestClass][Ignore]
public abstract class EqualsFixtureBase<T>
{
....
答案 2 :(得分:7)
开箱即用,看起来单元测试继承只有在基本测试类与派生类在同一个程序集中时才有效。对我来说,这通常会违背拥有基类的目的。我也想知道为什么在博客上没有更多关于这个的帖子,如果我可能遗漏了什么。
您可以通过将基类链接到要使用它的每个项目来解决此问题。也许将其标记为内部,因此多份副本不会相互干扰。
还可以扩展TestClassExtensionAttribute
以挂钩到测试执行引擎。我尝试使用它来反映测试类并加载基类的测试,但很多类都没有记录,我无法使它工作。
答案 3 :(得分:0)
如果将基类'程序集放在与派生文件夹相同的文件夹中,它是否有效? 也许这就是为什么把它们放在同一个装配工程中;另一个程序集在他们想要的时候无法解析。我不确定如何在需要时建立您可能需要的正确探测路径。 .testsettings可以表示类似于appbase的内容和针对跑步者appdomain的探测器,也许正确设置的那些将帮助它绑定到基类程序集(如果与根派生单元测试程序集不同)。