🌠

TypeScriptのジェネリクスによりデシジョンテーブルを効率的に生成できるツールを開発した

2022/09/04に公開

これで何が嬉しいのか

  • ソフトウェアテストで利用される「デシジョンテーブル(参考)」の各セルに対して、期待結果を考えて手入力する作業を自動化できる。
    • 人的ミスを減らせる。
    • 高速化できる。
  • 各キーには日本語も利用できるので、可読性を維持できる。
  • Typescriptのジェネリクスにより、入力データ同士の整合性は保証され、IDEによる補完もされるため、テストコードの作成が超楽

なぜ作ったか

自分が開発したこれまでのウェブサービスにおけるブラウザテストでは、Excelで作成したデシジョンテーブルを読ませて効率化を図っていましたが、デシジョンテーブルの作成自体が大変でした。そのうち、テーブル作成は自動化すべきと考えるようになりました。

なぜTypescriptか

はじめは、デシジョンテーブル生成ツールをjsで開発しましたが、データの誤入力に起因するエラーが頻発し、その対応に時間がとられ、思ったほど楽になりませんでした。
Typescriptのジェネリクスを利用すれば、入力データ間の整合性を保証できると考えました。

どんなものが出力されるのか

出力形式は、オブジェクトの配列です。1つのオブジェクトが1つのテストケースに相当します。

[
	{
		'Condition.User.IsRegistered': 'True',
		'Condition.User.IsAdmin': 'True',
		'Condition.Device': 'Mobile',
		ExpectedResult: 'Failure',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '1'
	},
	{
		'Condition.User.IsRegistered': 'True',
		'Condition.User.IsAdmin': 'True',
		'Condition.Device': 'PC',
		ExpectedResult: 'Success',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '2'
	},
	{
		'Condition.User.IsRegistered': 'True',
		'Condition.User.IsAdmin': 'False',
		'Condition.Device': 'Mobile',
		ExpectedResult: 'Failure',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '3'
	},
	{
		'Condition.User.IsRegistered': 'True',
		'Condition.User.IsAdmin': 'False',
		'Condition.Device': 'PC',
		ExpectedResult: 'Success',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '4'
	},
	{
		'Condition.User.IsRegistered': 'False',
		'Condition.User.IsAdmin': 'False',
		'Condition.Device': 'Mobile',
		ExpectedResult: 'Failure',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '5'
	},
	{
		'Condition.User.IsRegistered': 'False',
		'Condition.User.IsAdmin': 'False',
		'Condition.Device': 'PC',
		ExpectedResult: 'Failure',
		Perspective: 'Only registered users accessed from PC can access.',
		ID: '6'
	}
]

これは、下記のデシジョンテーブルに相当します。(このMarkdown形式のテーブルも出力できます)

#1 #2 #3 #4 #5 #6
Condition.User.IsRegistered True X X X X - -
False - - - - X X
Condition.User.IsAdmin True X X - - - -
False - - X X X X
Condition.Device Mobile X - X - X -
PC - X - X - X
ExpectedResult Success - X - X - -
Failure X - X - X X
Any - - - - - -

これらはジェネリクスにより型付けされているので、参照時のキーや値の指定ミスは実行前に検知できます。
例えば、以下のようなありえないキーについては、VSCodeなどの静的解析でエラーになります。

let isRegistered;
isRegistered = tests[0].Conditiob.User.IsRegistered;	// error
isRegistered = tests[0].Condition.Usef.IsRegistered;	// error
isRegistered = tests[0].Condition.User.IsRegisteref;	// error
isRegistered = tests[0].Condition.User.IsRegistered;	// ok

以下のようなありえない値についても同様です。

if(tests[0].Condition.User.IsRegistered === 'false'){	// error
if(tests[0].Condition.User.IsRegistered === 'False'){	// ok

さらに、VSCode等による入力補完も効くため、効率的な取扱が可能です。

テストケースの可読性を維持するためにはキー名は長くなりがちですので、これらは意外と役に立ちます。

どんなものを入力するのか

以下の4つの入力を用意します。

  1. 各因子の水準
  2. 各因子のデフォルト値
  3. 各因子の値同士のありえない組み合わせ
  4. テストの観点

1. 各因子の水準

各因子(期待結果も含む)の水準、即ち、とりうる値です。
唯一、ジェネリクスによる型付けがされていない入力であり、末端要素が「文字列の配列」であるオブジェクトでさえあれば、どのような構造をとっても構いません。

例:
const domain = {
	"Condition":{
		"User":{
			"IsRegistered":[
				"True",
				"False"
			],
			"IsAdmin":[
				"True",
				"False"
			]
		},
		"Device":[
			"Mobile",
			"PC"
		]
	},
	"ExpectedResult":[
		"Success",
		"Failure",
		"Any"
	]
} as const;
type ExampleDomain = typeof domain;

2. 各因子のデフォルト値

全因子のデフォルト値です。因子の値が明示されない場合にのみ、参照されます。各因子の水準からひとつ値を選択すればよいです。

例:
const defaults : Defaults<ExampleDomain> = {
	"Condition":{
		"User":{
			"IsRegistered" : "True",
			"IsAdmin" : "False"
		},
		"Device":"Mobile"
	},
	"ExpectedResult":"Any"
} as const;

なお、各因子の水準もとにジェネリクスで型付けされているため、不正な指定をすると下記のようにエラーとなります。

3. 各因子の値同士のありえない組み合わせ

因子のありえない値の組み合わせです。ここで指定した組み合わせに該当するものは、出力結果から除外されます。下記の例では1つしか指定していませんが、組み合わせは複数指定できます。

例:
const exclusions:Exclusions<ExampleDomain> = [
	{
		"Condition.User.IsRegistered" : "False",
		"Condition.User.IsAdmin" : "True"
	}
] as const;

なお、各因子の水準をもとにジェネリクスで型付けされているため、不正な指定をすると下記のようにエラーとなります。

4. テストの観点

一般的にテストケースは「何をチェックすべきか」という「観点」に基づいて定義されます。このツールでは、観点を指定することでテストケースの生成方法を指定します。

  • title:観点のタイトル。
  • constants:組み合わせを網羅するときに定数としたい因子を指定します。具体的な値も指定します。
  • variables:組み合わせを網羅するときに変数としたい因子を指定します。
  • expect:各因子の値に応じて期待結果を判断し、指定します。

例では、1つしか指定していませんが、観点は複数指定できます。

例:
const perspectives:Perspectives<ExampleDomain> = [
	{
		"title": "Only registered users accessed from PC can access.",
		"constants": {},
		"variables": [
			"Condition.User.IsRegistered",
			"Condition.User.IsAdmin",
			"Condition.Device",
		],
		"expect": (test:Test<ExampleDomain>)=>{
			if(
				test["Condition.User.IsRegistered"] === "True" &&
				test["Condition.Device"] === "PC"
			){
				test["ExpectedResult"] = "Success";
			}else{
				test["ExpectedResult"] = "Failure";
			}
			return test
		},
	}
] as const;

なお、各因子の水準をもとにジェネリクスで型付けされているため、不正な指定をすると下記のようにエラーとなります。

テストケース数の爆発を抑える仕組み

因子数や水準数が増えると、指数関数的に組み合わせが増えていくため、すべてテストするのは現実的ではない場合があります。

本ツールでは、CoveringArrayの考え方におけるstrengthを観点ごとに指定できるので、比較的有効性の劣るテストケースを排除し、合計のテストケース数を削減することができます。

strengthは、「2」から「variablesの数」までの自然数です。小さいほど、テストケース数は小さくなります。「2」の場合は、オールペア法と等価となり(おそらく)、「variablesの数」の場合は指定しない場合と同様、全網羅となります。

どこで公開しているのか

GitHubnpmにて公開しています。

今後の予定

実は、現状のものがほぼできあがったあとで、microsoftのPICTというツールや、TableDrivenTestsという文化の存在を知ったので、今後はこれらに馴染みのあるユーザでも利用しやすいよう、適宜仕様を変更する予定です。

また、テストをサポートするツールなのに自身に対するテストコードがまだ用意できていないため、勉強しつつ対応する予定です。

Discussion