テストコード用のデータ作成でズボラしたら、テストがPASSしちゃった話

テストコード用のデータ作成でズボラしちゃダメという話です。
2020.07.14

LambdaでCSV形式のデータを扱うことがありました。 その処理に対するテストコードを書いていたのですが、少し楽をしたがためにテストがPASSしちゃったのです。

起こったこと

CSV(テキストデータ)の各行に対して、「列数が期待通りでなければNG」の判定を行っていました。(下記関数は簡略化しています)

  1. 1行ずつ取り出して
  2. 1要素ずつに分解して
  3. 要素数をチェックする

app.py

def is_validate_csv_format(raw_data: str):
    for line in raw_data.split('\n'):
        item = line.split(',')
        if len(item) != 3:
            return False
    return True

このとき、下記のテストコードでテストを行ってPASSしました。「改行文字付きの文字列」を作成するのを楽にするため、join()を使って改行文字を結合しています。

test_app.py

import pytest
import app

class TestValidateCsvFormat(object):
    @pytest.mark.parametrize(
        'csv, expected', [
            (
                '\n'.join([
                    'aaa,bbb,ccc',
                    'xxx,yyy,zzz'
                ]), True
            ),
            (
                '\n'.join([
                    'aaa,bbb',
                    'xxx,yyy,zzz'
                ]), False
            ),
        ]
    )
    def test_normal(self, csv, expected):
        ret = app.validate_csv_format(csv)
        assert expected == ret

しかし、処理内容として下記のような「改行文字が末尾にあるCSVデータ」は問題ないCSVデータとしたいのですが、上記のテストではそもそもテストしていませんでした。その結果として、「改行文字が末尾にあるCSVデータ」を扱った際に不具合として現れたのです。

aaa,bbb,ccc\nxxx,yyy,zzz\n

どうしてこうなった

テストコードで扱うデータについて、楽をするためにjoin()を使って改行文字を連結していたことが原因です。 join()は便利な関数ですが、あくまでも連結です。そのため最後の文字として改行\nは付与されません。

>>> '\n'.join(['111','222','333'])
'111\n222\n333'

これによって、「改行文字が末尾にあるCSVデータ」に対するテストが漏れていたのです。

さきほどのテストコードについて、「改行文字が末尾にあるCSVデータ」のテストケースを1件追加して実行すると、追加したテストはFailedになりました。

test_app.py

import pytest
import app

class TestValidateCsvFormat(object):
    @pytest.mark.parametrize(
        'csv, expected', [
            (
                '\n'.join([
                    'aaa,bbb,ccc',
                    'xxx,yyy,zzz'
                ]), True
            ),
            (
                '\n'.join([
                    'aaa,bbb',
                    'xxx,yyy,zzz'
                ]), False
            ),
            (
                'aaa,bbb,ccc\naaa,bbb,ccc\n', True  # これがFailedになる
            ),
        ]
    )
    def test_normal(self, csv, expected):
        ret = app.validate_csv_format(csv)
        assert expected == ret

テストがPASSするようにコードを修正する

「Pythonの空文字列は偽となることを利用して、下記のように修正すれば楽だよ」と教えてもらいました。

app.py

def validate_csv_format(raw_data: str):
    for line in [x for x in raw_data.split('\n') if x]:
        item = line.split(',')
        if len(item) != 3:
            return False
    return True
>>> bool('')
False

テストコードもjoin()を使わずデータを作成し、PASSすることを確認しました。

test_app.py

import pytest
import app

class TestValidateCsvFormat(object):
    @pytest.mark.parametrize(
        'csv, expected', [
            ('aaa,bbb,ccc\nxxx,yyy,zzz', True),
            ('aaa,bbb\nxxx,yyy,zzz', False),
            ('aaa,bbb,ccc\naaa,bbb,ccc\n', True),
        ]
    )
    def test_normal(self, csv, expected):
        ret = app.validate_csv_format(csv)
        assert expected == ret

教訓

テストコードで扱うデータは、めんどくさくてもしっかりと書きましょう。