web-dev-qa-db-ja.com

単体テストと例外処理の違いは何ですか

ユニットテストと例外処理の違いを理解するために、丸2日を費やしましたが、理解できません。

私が理解した(または私が理解したと思う)こと)

  • ユニットテストは、関数とクラスをテストします
  • pythonのunittestモジュールで実行できます
  • 例外は構文エラーです
  • 例外は、メインの.pyファイル内のtry/exceptステートメントで処理できます
  • 例外にはさまざまなタイプがあります
  • 一般的な例外を発生させず、常にタイプを指定する

私が取得しないもの

  • 単体テストの時期と内容
  • 私が使用できるかどうか-そしてそれが良いか悪いか-単体テストのステートメントを試して/除外する

  • 両方がまったく同じ目標を達成することを目的としている、つまりより「読みやすい」方法で例外を処理することが真実である場合

  • それらが2つの異なるものである場合

  • それらを一緒に使用する必要がある場合

たとえば、ターン属性を持つゲームクラスがあります。私がやりたいのは、ターン値が正の数であることをテストすることです。負になることはないことはわかっていますが、これを使用してテストの練習をしたいだけです。

現実の世界では、メインコードでtry/exceptステートメントを使用するか、それともユニットテストを実行する必要がありますか。

このコードを含むメインファイルがあります。

import pygame

# initialize
pygame.init()

# set the window
window_size = (800, 800)
game_window = pygame.display.set_mode(window_size)
pygame.display.set_caption('My Canvas')

# define colours
colours = {
    'black': (0, 0, 0),
    'white': (255, 255, 255),
    'gold': (153, 153, 0),
    'green': (0, 180, 0)
}


class Game:
    def __init__(self):
        self.turn = 0
        self.player = None
        self.winner = None

    # getter methods
    def get_turn(self):
        try:
            assert self.turn >= 0
        except ValueError:
            print('turn number must be positive')

    def get_player(self):
        return self.player

    def get_winner(self):
        return self.winner

    # setter methods

    def set_turn(self, turn):
        self.turn = int(turn)

    def set_player(self, player):
        self.player = player

    def set_winner(self, winner):
        self.winner = winner

そして、そのすぐ隣にtest.pyファイルが開いているので、次のコードですぐにtestを記述できます。

import unittest
from canvas import Game


class TestPlayer1(unittest.TestCase):

    def setUp(self):

        # Game objects
        self.game_turn_0 = Game()
        self.game_turn_5 = Game()
        self.game_turn_negative = Game()

        # values
        self.game_turn_0.turn = 0
        self.game_turn_5.turn = 5
        self.game_turn_negative.turn = -2

    def test_get_turn(self):
        self.assertEqual(self.game_turn_0.get_turn(), 0)
        self.assertEqual(self.game_turn_5.get_turn(), 5)
        with self.assertRaises(ValueError):
            self.game_turn_negative.get_turn()


if __name__ == '__main__':
    unittest.main()

説明をお願いします。

ありがとうございました

2
Mirko Oricci

get_turnメソッドを見てみましょう。

def get_turn(self):
    try:
        assert self.turn >= 0
    except ValueError:
        print('turn number must be positive')

ここにはいくつかの問題があります:

  • ほとんどの言語では、アサーションはデバッグモードでのみ実行されます。 Python= -Oフラグを指定して実行すると、アサーションは実行されません。
  • アサーションが失敗すると、AssertionErrorではなくValueErrorが発生します
  • そもそも、これが無効な状態になることは許されるべきではありませんでした。 self.turnが負の場合、実際のバグは別の場所で発生しています。

これが私がそれを書き直す方法です:

def set_turn(self, turn):
    if turn < 0:
        raise ValueError('Turn number must be nonnegative')
    self.turn = int(turn)

def get_turn(self):
    assert self.turn >= 0   # This should never fail. If it does, you made a programming mistake.
    return self.turn

そして、単体テストでは、期待どおりに動作することを確認するための呼び出しを記述できます。

3
user214290

例外処理と単体テストは、さまざまな問題を解決します。

  • 例外処理:実行中のプログラムが例外的な状態から回復できるようにします(ファイルを開くことができるが、別のプログラムで開いているなど)。
  • ユニットテスト:プログラムの小さなパーツ(ユニット)の動作をテストして、期待どおりに動作することを確認します。

例外処理は実行時の問題ですが、単体テストはビルド時の問題です。

二人は一緒に働くことができます。たとえば、ある場所から別の場所にファイルを移動しようとするユーティリティがあり、それらが少しずつ同じであることを確認するとします。ユーティリティでは、使用可能なファイルをスキャンして、それらを1つずつ処理する必要があります。

ユーティリティがファイルをコピーできない原因はいくつかあります。

  • ファイルは別のプロセスで使用されています
  • 現在の場所からファイルを削除する権限、または宛先に書き込む権限がありません
  • 一度に複数のユーティリティを実行している場合、ループでファイルを取得するまでにファイルが失われる可能性があります

それらはすべて例外です。これらの例外が原因でユーティリティの処理が停止するかどうかを判断する必要があります。たとえば、最後の例外は、途中で停止したくないものであり、別のプロセスにファイルがある場合は、ファイルをスキップしても問題ありません。権限の問題は、ユーティリティがその仕事をすることを本当に妨げているので、それが事実であるなら、あなたは止める必要があります。

単体テストでは、これらの各条件が満たされるシナリオを設定し、アプリケーションが適切に動作することをassertします。

  • 単体テスト1は、コピーを成功させるためにすべてがセットアップされるソースおよび宛先ディレクトリーをセットアップします。それは機能し、例外処理は必要ありません!
  • 単体テスト2では、ソースディレクトリと宛先ディレクトリを設定し、ソースファイルをロックします。それは失敗するので、ロックを処理するためにユーティリティに例外処理を追加する必要があります(つまり、既存のファイルをスキップして次に進みます)
  • ユニットテスト3-5は、ソースと宛先のディレクトリおよびソースファイルをセットアップしますが、権限(ソース、宛先、両方)を変更し、ファイルをコピーできないことを認識するとすぐにユーティリティが停止することをアサートします。以前に行った例外処理が広すぎない場合は、これでうまくいくはずです。そうでない場合は、アプリケーションを変更してすべてのテストに合格する必要があります。
  • 単体テスト6は、ソースディレクトリのスキャンがいつ完了するかを知る必要があるため、インスツルメントが少し難しくなりますが、ソースディレクトリと宛先ディレクトリ、ソースファイルをセットアップし、スキャンが完了した後、ソースファイルを削除します。移動操作が実行されました。アプリケーションが早期に終了する場合は失敗するため、コピー/検証/削除操作が完了する前に、例外処理を追加するか、少なくともexistsチェックを行う必要があります。

ご覧のとおり、これらはアプリケーションを改善するための相互に排他的な概念ではありません。ただし、目的は非常に異なります。


単純なifステートメントの方が適切な場合は、例外処理を使用すると言います。コードを変更する必要があります。指定した例では、次のようなスニペットがありました。

    try:
        assert self.turn >= 0
    except ValueError:
        print('turn number must be positive')

これはassertの誤用です。特に、例外をメソッドから離れさせないためです。次のようにすると、よりクリーンになり、(特にループで)より速く実行できます。

    if selt.turn < 0:
        print('turn number must be positive')

そのコードは、より読みやすい方法で同じ効果を提供し、例外処理のオーバーヘッドがありません。例外処理は高価です。

これで、ゲームが未確定の状態にあるためにゲームを停止する例外的なケースにしたい場合は、例外をnotでキャッチする必要がありますそのコードを呼び出して、コードを呼び出している人にそれを処理させます。これは単に次のようになります。

    assert(self.turn >= 0, 'turn number must be positive')
5
Berin Loritsch