テストと製品コードの間で定数を複製しますか?


20

テストと実際のコード間でデータを複製するのは良いですか、悪いですか?たとえば、FooSaver特定の名前のファイルを特定のディレクトリに保存するPythonクラスがあるとします。

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

私のテストでは、これらのファイルがすべて作成されたことを確認したいので、次のように言いたいと思います。

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

これは2つの場所でファイル名を複製しますが、それは良いと思います:反対側に出てくると予想されるものを正確に書き留めることを強制し、タイプミスに対する保護の層を追加し、一般的に物事が機能していると確信させますまさに期待どおり。将来変更a.foo_fileする場合type_a.foo_fileは、テストで検索と置換を行う必要があることを知っていますが、それがあまりに大したことではないと思います。コードとテストの理解が同期していることを確認することと引き換えにテストを更新するのを忘れた場合、むしろ誤検出がいくつかあります。

同僚は、この複製が悪いと考えているので、両側を次のようなものにリファクタリングすることをお勧めします。

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

そしてテストで:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

コードが期待どおりに動作していることを確信できないので、これが好きではありません--- out_dir + name実稼働側とテスト側の両方でステップを複製しました。+文字列がどのように機能するかについての私の理解の誤りを明らかにすることはなく、タイプミスもキャッチしません。

一方、これらの文字列を2回書き込むよりも明らかに脆弱ではなく、そのような2つのファイルにデータを複製するのは少し間違っているように思えます。

ここには明確な先例がありますか?テストと実稼働コード間で定数を複製しても大丈夫ですか?それとも脆すぎますか?

回答:


16

私はそれがあなたがテストしようとしているものに依存していると思う、それはクラスの契約が何であるかに行く。

クラスのコントラクトが正確にFooSaver生成される場合a.foo_fileし、b.foo_file特定の場所に、あなたはすなわち、直接それをテストするテストで定数を複製する必要があります。

ただし、クラスの契約により、一時領域に2つのファイルが生成され、それぞれの名前が特に実行時に簡単に変更される場合、テストから除外された定数を使用して、より一般的にテストする必要があります。

したがって、より高いレベルのドメイン設計の観点から、クラスの本質と契約について同僚と議論する必要があります。同意できない場合、これはテストではなく、クラス自体の理解および抽象化レベルの問題であると言えます。

また、リファクタリング中にクラスのコントラクトが変化することを見つけることも合理的です。たとえば、その抽象化レベルは時間とともに上昇します。最初は、特定の一時的な場所にある2つの特定のファイルについてですが、時間が経つにつれて、追加の抽象化が必要になることがあります。そのような場合、テストを変更して、クラスのコントラクトと同期するようにします。あなたがそれをテストしているからといって、クラスのコントラクトをすぐにオーバービルドする必要はありません(YAGNI)。

クラスのコントラクトが適切に定義されていない場合、テストを行うことでクラスの性質に疑問を抱くことができますが、それを使用することもできます。私はあなたがそれをテストしているという理由だけでクラスの契約をアップグレードすべきではないと言うでしょう。ドメインの弱い抽象化など、他の理由でクラスのコントラクトをアップグレードする必要があります。アップグレードしない場合は、そのままテストします。


4

@Erikが提案したこと-テストしているものが明確であることを確認するという点で-は、最初の考慮事項であるべきです。

しかし、あなたの決定が定数の因数分解の方向にあなたを導き、それがあなたの質問の興味深い部分を残します(言い換え)「なぜコードを複製するために定数を複製することをトレードオフすべきですか?」(「out_dirを複製する[名前]ステップ」について説明する場所を参照してください。)

(モジュロエリックのコメント)ほとんどの場合、重複する定数を削除することでメリット得られると思います。ただし、コードを複製しない方法でこれを行う必要があります。特定の例では、これは簡単です。本番コードでパスを「生の」文字列として扱う代わりに、パスをパスとして扱います。これは、文字列の連結よりもパスコンポーネントを結合するより堅牢な方法です。

os.path.join(self.out_dir, name)

一方、テストコードでは、このようなものをお勧めします。ここでは、パスがあり、リーフファイル名を「プラグイン」していることを強調しています。

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

つまり、言語要素をより慎重に選択することにより、コードの重複を自動的に回避できます。(常にではありませんが、私の経験では非常に頻繁に。)


1

Erik Eidtの答えに同意しますが、3番目のオプションがあります。テストで定数をスタブ化するため、実動コードで定数の値を変更してもテストはパスします。

Python unittestの定数のスタブを参照)

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

そして、このようなことをするとき、私は通常、withステートメントなしでテストを実行する健全性テストを行い、「 'a.foo_file'!= 'unique_to_your_test_a'」を確認し、withステートメントをテストに戻しますそれで再び通過します。

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