単体テストでnullパラメータを使用してオブジェクトを構築しても問題ありませんか?


37

現在のプロジェクトのユニットテストをいくつか書き始めました。私は実際にそれを経験していません。最初に完全に「取得」したいので、現在IoCフレームワークもモックライブラリも使用していません。

ユニットテストでオブジェクトのコンストラクターにnull引数を提供することに何か問題があるのではないかと思っていました。サンプルコードを提供します。

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としMotor、そのことを行うために。それはまったく興味がないCarRadioので、そのためにヌルを提供しました。

完全に構築されずに正しい機能提供するオブジェクトが、SRPの違反またはコードの匂いであるかどうか疑問に思っています。私はこのしつこい感じを持っていますが、speedLimit.IsViolatedBy(motor)どちらかと言えば正しくありません-モーターではなく車が速度制限に違反しています。全体の意図全体の一部のみをテストすることであるため単体テストと作業コードでは異なる視点が必要なのかもしれません。

単体テストでnullを使用してオブジェクトを構築すると、コードのにおいがしますか?


24
少しトピックから外れてMotorいますが、おそらくまったくありませんspeed。現在のとに基づいて、throttleとを計算する必要があります。それを使用して、それを現在の速度に統合し、それをに戻して信号に戻すのは、車の仕事です...しかし、とにかく、あなたはリアリズムのためにそこにいなかったのですか?torquerpmthrottleTransmissionrpmMotor
cmaster

17
コードの匂いは、コンストラクターがそもそも文句を言わずにnullを取ることです。これが許容範囲内であると思われる場合(これには理由があるかもしれません):さあ、テストしてください!
トミー

4
Null-Object patternを検討してください。多くの場合、コードを簡素化し、NPEセーフにします。
バクリウ

17
おめでとうございます:あなたはnullラジオで速度制限が正しく計算されていることをテストしました。ここ、無線で速度制限を検証するテストを作成できます。動作が異なる場合に備えて...
Matthieu M.

3
この例についてはわかりませんが、クラスの半分だけをテストしたいという事実は、何かを分割する手がかりになることがあります
-Owen

回答:


70

上記の例の場合、がなくてもa Carが存在することは理にかなっていますCarRadio。その場合、nullを渡すことが許容される場合があるだけでなくCarRadio、必須であると言います。テストでは、Carクラスの実装が堅牢であり、存在しない場合にNULLポインター例外をスローしないことを確認する必要がありますCarRadio

ただし、別の例を見てみましょう-を考えてみましょうSteeringWheel。aにa Carが必要であると仮定しましょうSteeringWheel、しかし速度テストはそれを本当に気にしません。この場合、null SteeringWheelを渡すCarことはありません。これは、クラスを移動するように設計されていない場所にプッシュするためです。この場合、DefaultSteeringWheel(メタファーを続けるために)ある種の直線を作成するほうがよいでしょう。


44
そしてことを確認するためにユニットテストを書くことを忘れないでくださいCarなしで構築することができませんSteeringWheel...
cmaster

13
私は何かを見逃しているかもしれませんが、を使用せずにを作成することが可能であり、一般的な場合でも、渡す必要がなく、明示的なnullをまったく心配する必要のないコンストラクタを作成することは意味がありません?CarCarRadio
ハサミムシ

16
自動運転車にはハンドルが付いていない場合があります。ただ言って。☺
ブラックジャック

1
@James_Parsons内部でのみ使用され、常にnull以外のパラメータを受け取るため、nullチェックを持たないクラスに関するものであることを忘れていました。の他のメソッドはCarnull CarRadioでNREをスローしますが、関連するコードはSpeedLimitそうではありません。現在投稿されている質問の場合、あなたは正解であり、私は実際にそれを行います。
R.シュミッツ

2
@mtraceur null引数付きの構築を許可するクラスはnullが有効なオプションであることを誰もが想定した方法で、おそらくそこにnullチェックがあるはずだと気づきました。私が実際に抱えていた問題は、ここで私が台無しにしたくないし、助けてくれた、多くの良い、興味深い、よく書かれた答えによってカバーされています。また、私は物事を未完成のままにしたくないので、この答えを今受け入れます。そして、それは最も賛成です(それらはすべて役に立ちましたが)。
R.シュミッツ

23

単体テストでnullを使用してオブジェクトを構築すると、コードのにおいがしますか?

はい。コードの匂いの C2定義に注意してください:「CodeSmellは、確実性ではなく、何かが間違っているかもしれないというヒントです。」

テストは正常に実行されます。SpeedLimitには、モーターを搭載した車が必要です。CarRadioにはまったく興味がないので、そのためにnullを提供しました。

コンストラクターに渡される引数にspeedLimit.IsViolatedBy(car)依存しないことを実証しようとしている場合CarRadio、おそらく、テストが呼び出された場合にテストに失敗するテストダブルを導入することで、その制約を明示的にすることは将来のメンテナーにより明確になるでしょう。

おそらくそうであるようにCarRadio、この場合は使用されないことがわかっているものを作成する作業を自分で保存しようとしている場合は、

  • これはカプセル化の違反です(CarRadio使用されていないという事実をテストに漏らしています)
  • Car指定せずに作成したいケースを少なくとも1つ発見しました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」と呼ばれ、AssertがSpeedLimitのメソッドをチェックしていることがわかります。

これは2番目の興味深い匂いです。SpeedLimitをテストするためには、おそらくSpeedLimitのチェックで気になるのは速度だけですが、ラジオの周りに車全体を構築する必要があります。

これは、車全体を必要とするのではなく、車が実装SpeedLimit.isViolatedByするロールインターフェイスを受け入れる必要があるヒントかもしれません。

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さえ渡すことは良いテストです。NULLモーターとして合格すると何が起こるかをテストで確認したいと思います。それはコードの匂いではなく、コードの匂いの反対です。テスト中です。


Carここでテストを書いているようです。私は間違った声明を検討するものは何も表示されませんが、問題はテストについてSpeedLimitです。
R.シュミッツ

@ R.Schmitzどういう意味かわかりません。質問を引用しましたが、それNULLはのコンストラクターに渡すことですCar
-nvoigt

1
テスト中にSpeedLimit他の何かにnullを渡すと、その約。テストは「SpeedLimitTest」と呼ばれ、唯一AssertのメソッドがのメソッドをチェックしていることがわかりSpeedLimitます。テストCar-たとえば、「あなたの車がラジオなしで構築可能である場合、そのためのテストを作成する必要があります」-別のテストで行われます。
R.シュミッツ

7

私は個人的にヌルを注入することをためらいます。実際、依存関係をコンストラクターに注入するときはいつでも、最初に行うことの1つは、それらの注入がヌルの場合、ArgumentNullExceptionを発生させることです。そうすれば、クラス内でそれらを安全に使用でき、何十ものnullチェックが広まることはありません。これは非常に一般的なパターンです。コンストラクターで設定された依存関係を後で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;
   }
}

上記を使用すると、nullチェックが期待どおりに機能することを確認するために、1つまたは2つのユニットテストを実行して、nullを挿入できます。

ただし、次の2つのことを行うと、これは問題になりません。

  1. クラスの代わりにインターフェイスにプログラムします。
  2. モックフレームワーク(C#のMoqなど)を使用して、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
これまでのところ、ダミーオブジェクトも契約を履行する必要があると言っています。場合はIMotor持っていたgetSpeed()方法を、DummyMotor成功した後に変更速度を返すために必要となるsetSpeedだけでなく、。
ComFreek

1

大丈夫というだけでなく、レガシーコードをテストハーネスに組み込むための、よく知られた依存関係を破る手法です。(Michael Feathersによる「レガシーコードの効果的な使用」を参照してください。)

匂いがする?まあ...

テストは香りがしません。テストはその仕事をしています。「このオブジェクトはnullでも、テスト対象のコードは引き続き正常に動作します」と宣言しています。

臭いは実装にあります。コンストラクター引数を宣言することにより、「このクラス適切に機能するためにこのことを必要とします」と宣言しています。明らかに、それは真実ではありません。間違っていることは少し臭いです。


1

最初の原則に戻りましょう。

単体テストは、単一のクラスのいくつかの側面をテストします。

ただし、他のクラスをパラメーターとして取るコンストラクターまたはメソッドがあります。これらの処理方法はケースごとに異なりますが、目的に正接しているため、セットアップは最小限にする必要があります。NULLパラメーターを渡すことが完全に有効であるシナリオがあります(ラジオのない車など)。

ここでは、レースラインをテストして(車のテーマを継続するために)単にテストしていると仮定しています、他のパラメーターにNULLを渡して、防御コードと例外処理メカニズムが機能していることを確認することもできます必須。

ここまでは順調ですね。ただし、NULL 有効な値ではない場合どうなりますか。さて、ここであなたはモック、スタブ、ダミー、偽物の暗い世界にいます。

これらすべてを実行するのが少し難しいと思われる場合、実際にはCarコンストラクターにNULLを渡すことで既にダミーを使用しています。これは、潜在的に使用されない値だからです。

すぐに明らかになるのは、多くのNULL処理を使用してコードをショットガンする可能性があるということです。クラスパラメータをインターフェイスに置き換えると、モックやDIなどの処理も​​簡単になります。


1
「単体テストは、単一のクラスのコードをテストするためのものです。」ため息。それは単体テストとは異なり、そのガイドラインに従うことは、肥大化したクラスや逆効果で妨害的なテストにあなたを導くでしょう。
アントP

3
「クラス」のe曲表現として「ユニット」を扱うことは残念ながら非常に一般的な考え方ですが、実際にはこれはすべて(a)「ガベージイン、ガベージアウト」テストを奨励します。 (b)自由に変更できるはずの境界を強制するため、生産性が低下する可能性があります。もっと多くの人が自分の痛みに耳を傾け、この先入観に挑戦することを願っています。
アントP

3
そのような混乱はありません。コードの単位ではなく、動作の単位をテストします。単体テストの概念については何もありません-ソフトウェアテストの広く普及している貨物カルトハイブマインドの場合を除いて-これは、テストユニットがクラスの明確なOOP概念に結合されていることを意味します。また、非常に有害な誤解もあります。
アントP

1
私はあなたが何を意味するのか正確には分かりません-あなたが暗示しているものの正反対を主張しています。ハード境界をスタブ/モックし(絶対に必要な場合)、動作ユニットをテストします。パブリックAPIを介して。そのAPIが何であるかは、構築しているものによって異なりますが、フットプリントは最小限でなければなりません。抽象化、ポリモーフィズム、または再構築コードを追加しても、テストが失敗することはありません。「クラスごとのユニット」はこれと矛盾し、そもそもテストの価値を完全に損なうことになります。
アントP

1
RobbieDee-私は正反対について話しています。あなたがよくある誤解を抱いているかもしれないと考えて、あなたが「ユニット」と考えるものを再訪し、多分、ケント・ベックがTDDについて話したときに実際に話していたことについてもう少し深く掘り下げてください。ほとんどの人が今TDDと呼んでいるものは、貨物カルトの怪物です。youtube.com/watch?v=EZ05e7EMOLM
Ant P

1

あなたのデザインを見てみると、Carがのセットで構成されていることをお勧めしFeaturesます。フィーチャー多分CarRadio、またはEntertainmentSystemそのようなものから構成することができるCarRadioDvdPlayerなど

したがって、少なくとも、Carのセットでを構築する必要がありますFeaturesEntertainmentSystemそして、CarRadioの(Java(登録商標)、C#等)インターフェースの実装となりpublic [I|i]nterface Feature、これは複合デザインパターンのインスタンスであろう。

そのため、常にCarwith Featuresを構築し、単体テストはCarwithoutをインスタンス化しませんFeaturesBasicTestCarFeatureSet最も基本的なテストに必要な最小限の機能セットを持つモックを検討することもできます。

いくつかの考え。

ジョン

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.