目的
unittestモジュールのsuitでは、テスト関数(プレフィックスtestで番号を末尾に書くとその順にテストしてくれる)を連ねておくと一連のテストを回してくれるのだが、やっているとだんだん同じような関数が量産されてくる。
これを回避するのにサブクラスを使ってfor文の中でぶん回す方法があるが、やってみると以下の不満が出てきた。
- ループ途中でコケる(assert関数でraiseされる)と残りのサブテストが丸ごとスキップされて次のテストに行ってしまう。
- サブテストの数をカウントできないので別途カウンタを用意する必要がある。
そこで、テスト関数をテストケースの数分だけ動的に生成し、テスト数が増えてもコードの変更を伴わないように以下のUT用のコードを作った。
(lambda上に書いたUT対象のコードをUT用lambdaからコールするケースで想定)
コード
ディレクトリ構成
.
├── errorCases
│    ├── errorCase1.py
│    ├── errorCase2.py
│    └── errorCase3.py
├── nomalCases
│    ├── nomalCase1.py
│    └── nomalCase2.py
└── lambda_function.py
test.pyのtest01関数(正常系)とtest02関数(異常系)の両方を、nomalCasesディレクトリ、errorCasesディレクトリに書かれたケース分全てを回して、その結果をreturnするlambdaである。
lambda_function.py
import unittest
import logging
import json
import boto3
import os
from importlib import import_module
logger = logging.getLogger()
logger.setLevel(logging.INFO)
nomalCasesNum = len(os.listdir(os.path.dirname(os.path.abspath(__file__)) + "/nomalCases"))
errorCasesNum = len(os.listdir(os.path.dirname(os.path.abspath(__file__)) + "/errorCases"))
def targetInvoke(evt):
    try:
        res = boto3.client('lambda').invoke(
            FunctionName='UT対象のlambda名',
            InvocationType='RequestResponse',
            Payload=json.dumps(evt)
        )
        actual = json.loads(res["Payload"].read())
        logger.info("******* actual ********")
        logger.info(actual)
        logger.info("***********************")
    except:
        import traceback
        traceback.print_exc()
        raise Exception("error")
    return actual
        
def createNomalTestMethods(caseNum):
    def testMethod(self):
        logger.info("Nomal test case: " + str(caseNum) + "/" + str(nomalCasesNum))
        fileName = "nomalCases.nomalCase" + str(caseNum)
        case = import_module(fileName)
        expected = case.expected
        logger.info("******* expected *******")
        logger.info(expected)
        logger.info("************************")
        actual = targetInvoke(case.params)
        self.assertEqual(expected, actual, "NOT MATCH")
    return testMethod
def createErrorTestMethods(caseNum):
    def testMethod(self):
        logger.info("Error test case: " + str(caseNum) + "/" + str(errorCasesNum))
        fileName = "errorCases.errorCase" + str(caseNum)
        case = import_module(fileName)
        expected = case.expected
        logger.info("******* expected *******")
        logger.info(expected)
        logger.info("************************")
        actual = targetInvoke(case.params)
        actual = json.loads(actual["errorMessage"])["errorMessage"]
        self.assertEqual(expected, actual, "NOT MATCH")
    return testMethod
    
def createTestClass():
    testClass = type('lambdaTest', (unittest.TestCase,), {})
    for caseNum in range(1, nomalCasesNum + 1):
        setattr(testClass,'test' + str(caseNum),createNomalTestMethods(caseNum))
    for caseNum in range(1, errorCasesNum + 1):
        setattr(testClass,'test' + str(caseNum + nomalCasesNum) ,createErrorTestMethods(caseNum))
    return testClass
    
def suite():
    test_suite = unittest.TestSuite()
    testClass = createTestClass()
    test_suite.addTest(unittest.makeSuite(testClass))
    return test_suite
def lambda_handler(event, context):
    testsuite = suite()
    runner = unittest.TextTestRunner()
    res = runner.run(testsuite)
    
    return {
        'statusCode': 200,
        'results': {
            "tests": res.testsRun,
            "errors": len(res.errors),
            "failures": len(res.failures)
        }
    }
errorCase1.py/nomalCase1.py
params = {
    #eventに渡すjson
}
expected = "hoge" # 期待する結果
サブテストforループ時のメモ
- suitのテストケースの中でfor文をぶん回す場合にはループの何番目でコケたのかがわかるように以下のようにサブテストを使う。(そうしないとログに出てこない。)
with self.subTest(caseNum=ループのイテレータ):
    assert関数〜〜
参考