在单元测试中使用空参数构造对象可以吗?


37

我开始为当前项目编写一些单元测试。我真的没有经验。我首先要完全“得到它”,所以我目前既不使用IoC框架也不使用模拟库。

我想知道在单元测试中向对象的构造函数提供空参数是否存在任何问题。让我提供一些示例代码:

public class CarRadio
{...}


public class Motor
{
    public void SetSpeed(float speed){...}
}


public class Car
{
    public Car(CarRadio carRadio, Motor motor){...}
}


public class SpeedLimit
{
    public bool IsViolatedBy(Car car){...}
}

另一个Car Code Example(TM)仅简化为对该问题重要的部分。我现在写了一个类似这样的测试:

public class SpeedLimitTest
{
    public void TestSpeedLimit()
    {
        Motor motor = new Motor();
        motor.SetSpeed(10f);
        Car car = new Car(null, motor);

        SpeedLimit speedLimit = new SpeedLimit();
        Assert.IsTrue(speedLimit.IsViolatedBy(car));
    }
}

测试运行正常。SpeedLimit需要Car用a Motor才能完成任务。它根本不感兴趣CarRadio,因此我为此提供了null。

我想知道一个没有完全构建就提供正确功能的对象是否违反了SRP或代码气味。我有这样的感觉,但speedLimit.IsViolatedBy(motor)感觉也不正确-汽车而不是汽车违反了速度限制。对于单元测试和工作代码,也许我只需要一个不同的角度,因为整个目的只是测试整个部分的一部分。

在单元测试中使用null构造对象会产生代码异味吗?


24
有点题外话,但Motor可能根本不应该有speed。它应该有一个,throttletorque根据当前rpm和计算a throttle。使用a Transmission将其集成到当前速度,并将其转化rpm为信号以回馈给汽车,这是汽车的工作Motor。但是我想,无论如何,您并不是出于现实,不是吗?
cmaster

17
代码的味道是,您的构造函数将null放在首位而无需抱怨。如果您认为这可以接受(您可能有这样做的理由):请继续进行测试!
汤米

4
考虑Null-Object模式。它通常可以简化代码,同时还可以使其成为NPE安全的。
巴库里

17
恭喜:您已经测试了null无线电,可以正确计算速度限制。现在,你可能要创建一个测试来验证限速无线电; 万一行为有所不同……
Matthieu M.

3
不确定这个例子,但是有时您只想测试半个班级这一事实就表明应该打破某些东西
Owen

Answers:


70

在上述示例的情况下,Car不存在a 可以存在a 是合理的CarRadio。在这种情况下,我想说的是,CarRadio有时传递一个null不仅可以接受,而且说这是必须的。您的测试需要确保Car该类的实现是可靠的,并且在不CarRadio存在任何情况时不引发空指针异常。

但是,让我们举一个不同的示例-考虑一个SteeringWheel。假设a Car必须具有一个SteeringWheel,但是速度测试并不真正在乎它。在这种情况下,我不会传递null,SteeringWheel因为这Car会将类推到其设计不可行的地方。在这种情况下,最好创建某种形式DefaultSteeringWheel(以继续隐喻)锁定在一条直线上。


44
而且不要忘了编写单元测试来检查a Car不能在没有SteeringWheel...的情况下构建
cmaster

13
我可能会缺少一些东西,但是如果有可能甚至创建不带a的通用对象,那么创建一个不需要传入任何值且不必担心显式null的构造函数是否有意义? ?CarCarRadio
土蚣

16
自动驾驶汽车可能没有方向盘。只是说。☺–
BlackJack

1
@James_Parsons我忘了补充一点,它是关于没有空检查的类,因为它仅在内部使用并且始终接收非空参数。的其他方法Car会抛出带有null的NRE CarRadio,但不会抛出相关代码SpeedLimit。对于现在发布的问题,您将是正确的,我的确会这样做。
R. Schmitz

2
@mtraceur每个人都认为该类允许使用null参数进行构造的方式意味着null是一个有效的选项,这使我意识到我应该在那里进行null检查。然后,我会遇到的实际问题被很多很好,有趣,写得很好的答案所覆盖,这些答案我不想破坏,但确实对我有所帮助。现在也接受这个答案,因为我不喜欢把事情还没完成,并且它是最受好评的(尽管它们都很有帮助)。
R. Schmitz,

23

在单元测试中使用null构造对象会产生代码异味吗?

是。注意代码气味的C2定义:“ CodeSmell暗示某些事情可能是错误的,而不是确定性。”

测试运行正常。SpeedLimit需要一辆装有电机的汽车才能完成其任务。它根本对CarRadio不感兴趣,因此我为此提供了null。

如果您试图证明它speedLimit.IsViolatedBy(car)不依赖于CarRadio传递给构造函数的参数,那么将来的维护者可能会更清楚地通过引入一个测试双精度来明确表示该约束,如果该双精度测试在调用时失败了。

如果更有可能的是,如果您只是试图保存自己的创建工作,而这种工作CarRadio在这种情况下并未使用,那么您应该注意

  • 这违反了封装(您要让CarRadio未使用的事实泄漏到测试中)
  • 您发现至少一种情况下,您想创建一个Car不指定CarRadio

我强烈怀疑代码试图告诉您您想要一个看起来像

public Car(IMotor motor) {...}

然后,该构造函数可以决定如何处理事实:CarRadio

public Car(IMotor motor) {
    this(null, motor);
}

要么

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

要么

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

等等

关于在测试SpeedLimit时是否将null传递给其他信息。您可以看到测试称为“ SpeedLimitTest”,唯一的声明是检查SpeedLimit的方法。

这是第二个有趣的气味-为了测试SpeedLimit,您必须在收音机周围建造整辆汽车,尽管SpeedLimit检查的唯一可能就是速度

这可能是暗示SpeedLimit.isViolatedBy应该接受car 实现角色接口,而不是需要整辆car。

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeed是一个很糟糕的名字 希望您的域语言会有所改善。

有了该接口,您的测试SpeedLimit可能会更加简单

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}

在您的最后一个示例中,我假设您必须创建一个实现该IHaveSpeed接口的模拟类,因为(至少在c#中)您无法实例化一个接口本身。
瑞安·塞尔

1
没错-有效的拼写取决于测试所用的Blub风格。
VoiceOfUnreason

18

在单元测试中使用null构造对象会产生代码异味吗?

没有

一点也不。相反,如果NULL是可用作参数的值(某些语言可能具有不允许传递空指针的构造),则应对其进行测试。

另一个问题是当您通过时会发生什么NULL。但这正是单元测试的目的:确保为每个可能的输入生成定义的结果。并且NULL 潜在的输入,因此您应该具有定义的结果,并且应该对此进行测试。

所以,如果你的车是应该是没有一个无线电施工的,你应该写一个测试。如果不是,并且应该抛出一个异常,你应该写一个测试。

无论哪种方式,是的,您都应该为编写测试NULL。如果您将其遗忘,那将是一种代码气味。那意味着您有一个未经测试的代码分支,您刚刚假设它会神奇地没有错误。


请注意,我同意其他答案,可以改善您的测试代码。但是,如果改进的代码具有作为指针的参数,则NULL即使对改进的代码进行传递也是一个很好的测试。我希望进行测试以显示通过NULL电机后会发生什么。那不是代码的味道,而是代码的味道的相反。正在测试。


好像您在Car这里编写回合测试。虽然我什么都看不到,但我认为陈述有误,但问题在于测试SpeedLimit
R. Schmitz

@ R.Schmitz不确定您的意思。我引用了这个问题,它是关于传递NULL给的构造函数的Car
nvoigt

1
它是关于在测试时SpeedLimit是否将null传递给其他东西。您可以看到测试称为“ SpeedLimitTest”,唯一Assert的检查方法是SpeedLimit。测试Car-例如“如果您的汽车应该可以在没有无线电的情况下建造,那么您应该为此编写测试”-将在另一种测试中进行。
R. Schmitz '18

7

我个人会不愿意注入null。实际上,每当我将依赖项注入到构造函数中时,如果这些注入为null,我要做的第一件事就是引发ArgumentNullException。这样,我可以在班级内安全地使用它们,而不会散布许多空检查。这是一个非常常见的模式。请注意,使用readonly以确保之后在构造函数中设置的那些依赖项不能设置为null(或其他任何值):

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

有了以上内容,我也许可以进行一两个单元测试,在其中注入空值,以确保我的空值检查能够按预期工作。

话虽如此,一旦您完成以下两项操作,这就不再是问题了:

  1. 编程为接口而不是类。
  2. 使用模拟框架(例如Moq for C#)注入模拟的依赖关系,而不是null。

因此,对于您的示例,您将拥有:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

有关使用Moq的更多详细信息,请参见此处

当然,如果您想先理解它而不先进行模拟,只要使用接口,您仍然可以做到。只需按照以下步骤创建一个虚拟的CarRadio和Motor,然后注入它们即可:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())

3
这就是我会写的答案
Liath

1
我什至可以说,虚拟对象也必须履行其合同。如果IMotorgetSpeed()方法,则DummyMotor在成功后也需要返回修改后的速度setSpeed
ComFreek

1

这不仅可以,而且是一种众所周知的依赖项打破技术,用于将遗留代码纳入测试工具。(请参阅Michael Feathers的“使用旧版代码有效工作”。)

闻到吗?好...

试验不闻。测试正在完成。它声明“此对象可以为null,并且被测试的代码仍然可以正常工作”。

气味在执行中。通过声明构造函数参数,您就是在声明“此类需要此东西才能正常运行”。显然,这是不正确的。任何不真实的东西都会有点臭。


1

让我们退回到第一原则。

单元测试测试单个类的某些方面。

但是,会有构造器或方法将其他类作为参数。视情况而定,处理方式会有所不同,但设置应尽量少,因为它们与目标相切。在某些情况下,传递NULL参数是完全有效的,例如没有无线电的汽车。

我在这里假设,我们只是在测试赛车线以开始(以继续汽车主题),但是您可能还想将NULL传递给其他参数,以检查是否有任何防御性代码和异常处理机制能够正常工作。需要。

到现在为止还挺好。但是,如果NULL 不是有效值,该怎么办。好吧,在这里,您将进入一个充满模拟,存根,假人和假货的黑暗世界

如果这一切看起来有点麻烦,那么实际上您已经通过在Car构造函数中传递NULL而使用了一个虚拟对象,因为该值可能不会被使用。

很快就会变得很清楚的是,您可能正在使用大量NULL处理来完善代码。如果用接口替换类参数,那么还将为模拟和DI等铺平道路,这将使它们更容易处理。


1
“单元测试用于测试单个类的代码。” 感叹。那不是单元测试,遵循该指导方针将导致您进入classes肿的类或适得其反的阻碍测试。
Ant P

3
不幸的是,将“单元”视为对“阶级”的委婉说法是一种非常普遍的思维方式,但实际上,所有这些行为是(a)鼓励“垃圾入,垃圾出”测试,这会完全破坏整个测试的有效性套件和(b)强制执行应自由更改的边界,这会削弱生产力。我希望更多的人会听到他们的痛苦并挑战这一成见。
Ant P

3
没有这种混乱。测试行为单位,而不是代码单位。除了在软件测试中广泛使用的软件外,关于单元测试的概念没有任何内容,这意味着将测试单元与类的明显的OOP概念耦合在一起。这也是一种非常有害的误解。
Ant P

1
我不确定您的意思是什么-我是在与您所暗示的完全相反。存根/模拟硬边界(如果绝对必须)并测试行为单元。通过公共API。该API的功能取决于您所构建的内容,但占用空间应尽可能小。添加抽象,多态性或重组代码不会导致测试失败-“每类的单元数”与此矛盾并从根本上破坏了测试的价值。
Ant P

1
RobbieDee-我说的恰恰相反。考虑到您可能是一个怀有普遍误解的人,重新审视您认为是“单位”的东西,也许更深入地了解肯特·贝克在谈论TDD时实际上在谈论什么。现在大多数人称为TDD的是货邪教怪兽。youtube.com/watch?v=EZ05e7EMOLM
Ant P

1

看一下您的设计,我建议a Car由一组组成Features。功能可能是CarRadioEntertainmentSystem也可能包含CarRadioDvdPlayer等内容。

因此,您至少必须构造一个Car带有的集合FeaturesEntertainmentSystem并且CarRadio将是的接口(Java,C#等)的实现者,public [I|i]nterface Feature并且这将是Composite设计模式的一个实例。

因此,您将始终构造一个Carwith,Features而单元测试将永远不会在Car没有任何实例的情况下实例化a Features。尽管您可能会考虑模拟BasicTestCarFeatureSet,但该模拟具有最基本的测试所需的最少功能集。

只是一些想法。

约翰

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.