目次
・概要
・本日の成果・考え
・最後に
概要
本日は前回実装したバリデーションクラスのUTを行いたいと思います。
クラスの責務もすごくシンプルなので、文字列を渡してその結果をアサーションする形でテストしていきたいと思います。
本日の成果
テストパターン
正常系
- 正常の文字列(全角)の場合正常
- CRLF/CRはLFで戻して正常
- 3000文字までなら正常
- LFおよびTABは正常
異常系
- nullの場合エラー
- 空の場合エラー
- 30001文字以上はエラー
- 制御文字(C0)が含まれている場合エラー
- 制御文字(DEL)が含まれている場合エラー
- 制御文字(NUL)が含まれている場合エラー
制御文字の動作を検証
今回確認する制御文字についてあまりよく知らないので検証してみることにしました。
以下の記事を参考にしました。
C0の確認
コマンド
printf 'ABC\x1BDEF\n'
ABC
EF
なにやら、改行されている上にDも表示されなくなっていますね。
本当に制御文字が影響しているか以下のコマンドで確認してみます。
printf 'ABC\x1BDEF\n' | od -An -t x1
41 42 43 1b 44 45 46 0a
大文字英数字の16進数は参考ページで言うと「4」の行であり、そこから左から右に順番にA→B→Cとなっています。
そのため、出力結果はABCのあとに1bが存在するため、制御文字が認識されていることが確認できます。
実行端末によって制御文字の影響はブレるため確実に発生するとはいえませんが、少なくともMac同系列のUNIX系では発生する可能性が十分にあると思います。
※どちらにしろ調査用や開発端末がMacなら発生すると思いますので、運用や開発に影響するかと。
Delの確認
ターミナルに以下のコマンドを実行しました。
printf 'ABC\x7FDEF\n'
参考にしたサイトだとDelの制御文字は0x7Fですが、文字列に組み込む場合は、¥x7Fで16進数の0x7Fを入れるという意味になるみたいです。
ABCDEF
削除文字として認識はできているみたいですが、前後の文字には影響しませんでした。
printf 'ABC\x7FDEF\n'| od -An -t x1
41 42 43 7f 44 45 46 0a
ちょっと調べた限りだと出力結果にわかりやすく表示させる方法が見つかりませんでした。
NULの確認
これまでと同じ流れをするので、出力結果に全てまとめてだします。
著者@MacBook-Pro ~ % printf 'ABC\x00DEF\n'
ABCDEF
著者@MacBook-Pro ~ % printf 'ABC\x00DEF\n' | od -An -t x1
41 42 43 00 44 45 46 0a
僕の端末のターミナルだとNULの後も出力されました。
一応NUL自体は認識されているみたいです。
著者@@MacBook-Pro ~ % printf 'ABCDEF\n' | wc -c
printf 'ABC\x00DEF\n' | wc -c
7
8
上記結果からNULの分バイト数も増えています。
とはいえ、これでは実際に表示に影響するかわからないです。
NULのリスクとして後続に不正アクセスのプログラミングが含まれるリスクがありますが、本当に含まれた上で認識できないか確認したいと思います。
ChatGptにCで以下のプログラミングを考えてもらいました。
※Javaだと普通にNUL後続も認識できるみたいで僕の実力では再現できませんでした。
<<'EOF'
#include <stdio.h>
#include <string.h>
/* 出力形式は常に同じ:指定された長さだけ hex で表示 */
static void dump_hex(const char *label, const unsigned char *p, size_t n) {
printf("%s len=%zu bytes: ", label, n);
for (size_t i = 0; i < n; i++) printf("%02X ", p[i]);
printf("\n");
}
int main(void){
/* 同一入力:NULを含むバイト列 */
const unsigned char s[] = { 'A','B','C', 0x00, 'D','E','F', 0x0A }; // "ABC\0DEF\n"
/* ① C文字列として扱う(長さ決定が strlen = NULで止まる) */
size_t n_strlen = strlen((const char*)s); // => 3
dump_hex("strlen()", s, n_strlen);
/* ② バイト列として扱う(長さ決定が sizeof = 全部) */
size_t n_sizeof = sizeof(s); // => 8
dump_hex("sizeof()", s, n_sizeof);
return 0;
}
EOF
strlen() len=3 bytes: 41 42 43
sizeof() len=8 bytes: 41 42 43 00 44 45 46 0A
これで一部実行処理ではNULの後続は認識できないことが確認できました。
テストソース
public class InputValidatorTest {
private final InputValidator validator = new InputValidator();
// =========================
// 正常系
// =========================
/**
* No1: 正常の文字列(全角) → OK
*/
@Test
public void validate_No1_normalZenkaku_OK() {
String input = "バリデーションテストNo1(全角)";
String result = validator.validateExecute(input);
assertEquals(input, result);
}
/**
* No2: CRLF / CR → LF 正規化 → OK
*/
@Test
public void validate_No2_normalizeLineEnding_OK() {
String input = "バリデーションテストNo2\r\n"
+ "バリデーションテストNo2\r"
+ "バリデーションテストNo2";
String result = validator.validateExecute(input);
assertEquals(
"バリデーションテストNo2\n"
+ "バリデーションテストNo2\n"
+ "バリデーションテストNo2",
result);
}
/**
* No3: 3000文字までは許容 → OK
*/
@Test
public void validate_No3_maxLengthJust_OK() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3000; i++) {
sb.append('a');
}
String input = sb.toString();
String result = validator.validateExecute(input);
assertEquals(input, result);
}
/**
* No4: LFおよびTABは許容 → OK
*/
@Test
public void validate_No4_tabAndLf_OK() {
String input = "バリデーションテストNo4\t"
+ "バリデーションテストNo4\n"
+ "バリデーションテストNo4";
String result = validator.validateExecute(input);
assertEquals(input, result);
}
// =========================
// 異常系
// =========================
/**
* No5: null → NG
*/
@Test
public void validate_No5_nullInput_NG() {
try {
validator.validateExecute(null);
fail("例外が投げられるべき");
} catch (IllegalArgumentException e) {
assertEquals("入力してください。", e.getMessage());
}
}
/**
* No6: trim後空 → NG
*/
@Test
public void validate_No6_trimEmpty_NG() {
try {
validator.validateExecute(" \n\t ");
fail("例外が投げられるべき");
} catch (IllegalArgumentException e) {
assertEquals("入力してください。", e.getMessage());
}
}
/**
* No7: 3001文字以上はエラー → NG
*/
@Test
public void validate_No7_overMaxLength_NG() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3001; i++) { // 3000超過
sb.append('a');
}
try {
validator.validateExecute(sb.toString());
fail("例外が投げられるべき");
} catch (IllegalArgumentException e) {
assertEquals("入力が長すぎます。最大3000文字です。", e.getMessage());
}
}
/**
* No8: 制御文字(C0)を含む → NG
*/
@Test
public void validate_No8_rejects_C0_ESC_NG() {
String input = "バリデーションテスト" + ((char) 0x1B) + "No8";
try {
validator.validateExecute(input);
fail("ESC(0x1B) を含む入力は例外になるべきです。");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("使用できない制御文字"));
}
}
/**
* No9: 制御文字(DEL 0x7F)を含む → NG
*/
@Test
public void validate_No9_rejects_DEL_NG() {
String input = "バリデーションテスト" + ((char) 0x7F) + "No9";
try {
validator.validateExecute(input);
fail("DEL(0x7F) を含む入力は例外になるべきです。");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("使用できない制御文字"));
}
}
/**
* No10: 制御文字(NUL 0x00)を含む → NG
*/
@Test
public void validate_No10_rejects_NUL_NG() {
String input = "バリデーションテスト" + ((char) 0x00) + "No10";
try {
validator.validateExecute(input);
fail("NUL(0x00) を含む入力は例外になるべきです。");
} catch (IllegalArgumentException e) {
// 実装メッセージ固定なら固定一致でもOK。方針に合わせてNotNullのみ。
assertTrue(e.getMessage().contains("使用できない制御文字"));
}
}
}
実行結果
最後に
バリデーションのUTを完了しました。
正直、テストパターンも入力パターンのみのためテストとしてはかなり楽なソースだと思います。
シンプルな処理故と思いますが、こういうテストで網羅できるようなソースばかりの実装ができれば、ソースを今後運用していく上で工数をかけずに済むなと実感しました。
以上、ここまでお付き合いありがとうございます。
次回は、バリデーションクラスを組み込んだ上で、ControllerクラスからAPI通信までが問題なく動くことを確認してリンクテスト2に戻りたいともいます。
