我的理解是每个Establish
应该只执行一次,但下面的代码显示它执行多次。我们正在嵌套类以提供一些分组,同时将Subject的单元测试保存在一个文件中。这看起来像是一个bug。
我们使用的是machine.specifications.runner.resharper shaper扩展和MSpec 0.9.1.
[Subject(typeof(string))]
internal class EstablishRunTwice {
Establish sharedContext = () => Console.WriteLine("Shared context");
internal class ScenarioA : EstablishRunTwice {
Establish scenarioAContext = () => Console.WriteLine("ScenarioA context");
internal class ScenarioAVariation1 : ScenarioA {
Because of = () => Console.WriteLine("ScenarioAVariation1 Because");
It it1 = () => Console.WriteLine("ScenarioAVariation1 It1");
It it2 = () => Console.WriteLine("ScenarioAVariation1 It2");
}
internal class ScenarioAVariation2 : ScenarioA {
Because of = () => Console.WriteLine("ScenarioAVariation2 Because");
It it1 = () => Console.WriteLine("ScenarioAVariation2 It1");
It it2 = () => Console.WriteLine("ScenarioAVariation2 It2");
}
}
internal class ScenarioB : EstablishRunTwice {
Establish context = () => Console.WriteLine("ScenarioB context");
Because of = () => Console.WriteLine("ScenarioB Because");
It it1 = () => Console.WriteLine("ScenarioB It1");
It it2 = () => Console.WriteLine("ScenarioB It2");
}
}
对于ScenarioAVariation1,结果如下:
Shared context
Shared context
ScenarioA context
Shared context
Shared context
ScenarioA context
ScenarioAVariation1 Because
ScenarioAVariation1 It1
ScenarioAVariation1 It2
当我们使用NUnit做自己的自定义上下文规范框架时,我们通过确保所有子类都是抽象的(在这种情况下,EstablishRunTwice和ScenarioA将是抽象的)来解决NUnit运行的问题,但是MSpec在尝试这样做时会抛出错误。
这是因为您既嵌套又继承了测试类。通常情况下,您可能纯粹出于组织目的而在c#中使用嵌套类,但它也会影响MSpec中的执行。这可能出乎意料,但确实符合它的声明式风格。事实上,在MSpec中通常根本不需要使用继承,除非你在不同的文件中重用功能。
只需删除示例中的继承并保留嵌套,您将看到输出如下:
Shared context
ScenarioA context
ScenarioAVariation1 Because
ScenarioAVariation1 It1
ScenarioAVariation1 It2
...
这使得在外部类的建立中使用公共设置和覆盖内部类中的特定部分变得容易。就我个人而言,在我意识到它是以这种方式工作之前,我觉得我是在与MSpec为依赖于不同设置的测试用例而战斗(与那些在Because中直接传递不同值的测试用例相比)。
假设你有一个天气传感器之类的东西,你可以这样构造它:
[Subject(typeof(WeatherSensor))]
class when_reading_the_sensor : WithSubject<WeatherSensor> {
Establish context = () => { common setup }
class with_sunny_conditions {
Establish context = () => { setup sunny conditions }
Because of = () => Subject.Read();
It should_say_it_is_sunny => () => ...
It should_return_correct_temps => () => ...
}
class with_rainy_conditions {
...
}
}
在测试结果中也读得很好。如果第二个测试失败,它可能在测试树中显示如下:
- (X)天气传感器,在阳光条件下读取传感器
- (✔)应该说今天是晴天
- (X)应该返回正确的温度
如果,就像在那个例子中一样,所有不同的条件纯粹来自于注入到Subject中的依赖的设置,您甚至可能希望将Because移到外部类中。然后,您可以在内部类中拥有一个Establish和一些it,使每个测试用例非常简洁。外部的Because仍然会在所有需要的established之后和Its之前为每个内部类运行。
这真是一种令人困惑的结构方式——聪明,但也许有点太聪明了。我发现很难阅读和理解其意图。事实上,我甚至无法想象编译器会用这个继承结构做什么,因此我无法理解其意图。我想你可能想多了。
让我看看,ScenarioA
不仅嵌套在EstablishRunTwice
中,而且还继承了它。这是否意味着它会一直继承嵌套的自身副本,直到无穷大?然后,ScenarioB
继承了所有这些!我的头要炸了。你得到令人困惑的结果,我一点也不惊讶。嵌套的给了你什么?它是否使代码更具可读性或更易于维护?我不相信它有。
使用KISS原则。通常的做法是将每个上下文放在自己的类中,没有嵌套;只需使用文件对相关测试进行分组,也可以使用[Subject]
属性中的Concern
参数作为另一种分组方式。如果有意义的话,您可以从其他上下文继承,但是在使用MSpec几年后,我慢慢地得出结论,太多的继承会损害可读性,并使测试代码更粘稠,所以明智地使用继承。
更新:在反思了我认为你想要实现的目标后,我怀疑你正在试图重新发明行为。这可能是MSpec的一个缺乏文档和理解的特性,它允许您定义一组公共行为,这些行为以后可以应用于多个测试上下文中。这听起来像是你想要达到的目标吗?下面是一个行为的例子:
[Behaviors]
internal class DenebRightAscension
{
It should_have_20_hours_ = () => UUT.Degrees.ShouldEqual(20u);
It should_have_41_minutes = () => UUT.Minutes.ShouldEqual(41u);
It should_have_59_seconds = () => UUT.Seconds.ShouldEqual(59u);
protected static Bearing UUT;
}
[Subject(typeof(HourAngle), "sexagesimal")]
internal class when_converting_hour_angle_to_sexagesimal
{
Because of = () =>
{
RaDeneb = 20.6999491773451;
UUT = new Bearing(RaDeneb);
};
Behaves_like<DenebRightAscension> deneb;
protected static Bearing UUT;
static double RaDeneb;
}
[Subject(typeof(Bearing), "sexagesimal")]
internal class when_converting_to_sexagesimal
{
Because of = () =>
{
RaDeneb = 20.6999491773451;
UUT = new Bearing(RaDeneb);
};
Behaves_like<DenebRightAscension> deneb;
protected static Bearing UUT;
static double RaDeneb;
}
注意,在behavior中字段是通过名称匹配的,而不是通过任何类型的继承。因此,行为神奇地知道我所说的'UUT'是什么意思,即使类是不相关的。