ユーザーネームとかで重複を許容しない場合に、
— Hideo Oide🍜 (@hide_ramen_san) 2024年3月28日
1. すでにAがある時に別の人がAを登録しようとする
2. BさんがAに変更しようとする
3. Aを削除してから別の人がAを登録しようとする
の3パターンは検証しないといけないと思う。
ただ、そういう話はテスト手法?としては体系化されてない気がする。
おいでさんのツイを見て、「あっ、これ!」と思いました。
テスト設計技法って、たとえばデシジョンテーブルテストとか状態遷移テストなどいろいろありますが、「テスト対象として割とよく遭遇する割には、”テスト設計技法”みたいな形ではまとめられていない」ケースがけっこうある気がするんですよね。
おいでさんの例は、シンプルでわかりやすいので、自分がどう考えているのかを整理してみました。
サマリ
- ありがちな仕様なのに、テスト設計の定石がそこまで広まってないものがある気がする。
- 「一意な値」パターンは、CRUDとライフサイクルの2つを考えるとよさそう。
- テスト設計は、仕様の曖昧さを減らすことのできる活動である。早く始めよう。
- 思いついたケースを無造作に追加していくのではなく、そのケースからテスト観点を逆導出すると、網羅性が考えやすくなる。
対象とする仕様
仕様は以下とします。ここでは「一意な値」パターンと呼んでおきましょう。
- ユーザは、固有のユーザIDを持っている。ユーザIDを変更することはできない。
- ユーザは、ユーザ登録時にユーザ名を決めることができる。
- ユーザは、ユーザ名を変更することができる。
- ユーザ名は一意であり、重複することができない。すでに使用されているユーザ名を使おうとするとエラーとなる。
これ以外の要素、たとえば以下のようなものは無視します。
- 文字列長、文字種といった、入力値の属性についての仕様
- 変更したユーザ名がシステム全体に反映されるまでの時間差みたいなもの
テスト設計1: まずCRUDを考える
おいでさんが例として挙げられている3つのテストケースは有効だと思います。
- すでにAがある時に別の人がAを登録しようとする
- BさんがAに変更しようとする
- Aを削除してから別の人がAを登録しようとする
ただわたしの性格的に、リスト型のテストセットは網羅性が見えづらく、テーブル型にしたくなります。
おいでさんも言及されている通り、「登録」「変更」というキーワードから、まずはいわゆるCRUD、Create(作成)、Refer(参照)、Update(更新)、Delete(削除)での整理をしたくなりますね。
仕様2・3を確認するためのテストケースは、こういうテーブルになります。単純な機能テストです。
作成 | 〇: ユーザ作成時に、ユーザ名をつけられる |
参照 | 〇: 作成・変更したユーザ名を参照できる |
変更 | 〇: ユーザ作成後に、ユーザ名を変更できる |
削除 | 〇: 作成・変更したユーザを削除できる |
次に、仕様4を考慮しましょう。横軸に、「使用されているかどうか」の要素を加えます。
使用されていないユーザ名 | 使用されているユーザ名 | |
---|---|---|
作成 | 〇: ユーザ作成時に、使用されていないユーザ名をつけられる | ×: 使用されていないユーザ名を指定したユーザを作成できない |
参照 | 〇: 作成・変更したユーザ名を参照できる | n/a: 作成・変更できないので対象外 |
変更 | 〇: 使用されていないユーザ名に変更できる | ×: 使用されていないユーザ名に変更できない |
削除 | 〇: 作成・変更したユーザを削除できる | n/a: 作成・変更できないので対象外 |
ここでもしかすると、「使用されている」の特殊パターンとして、「自分自身が使用している」というケースを思いつくかもしれません。言い換えると「ユーザ名の変更において、現在と同じユーザ名を指定する」ものです。この結果は、
- 問題なく「変更」処理を完了させる
- 「変更前と後で同じユーザ名である」旨のエラーメッセージを出す
- 「ユーザ名が重複している」旨のエラーメッセージを出す
といくつか考えられるので、仕様を確認するいい機会かもしれません。テスト設計はできるだけ早く始めましょう。なお、わたしは2.が好きです。
テスト設計2: ユーザ名の状態の分解能を上げる
おいでさんのツイの3つ目
- Aを削除してから別の人がAを登録しようとする
も大事ですよね。ですが、上の表の中にはうまく表現できていません。
このような場合、思いついたケースを単品で追加するのではなく、そのケースがどのような観点に属するものなのか考えることが大切です。
表をよく見ると、「使用されていない」には2つの状態が混在しているることに気づきます。
- 過去から現在まで一度も使用されていない
- 過去に使用されていたが、現在は使用されていない
「使用されていない」状態の分解能を上げると、テーブルはこのようになります。
一度も使用されていないユーザ名 | 現在使用されているユーザ名 | 過去に使用されていたが今は使用されていないユーザ名 | |
---|---|---|---|
作成 | 〇: ユーザ作成時に、一度も使用されていないユーザ名をつけられる | ×: 現在使われているユーザ名を指定したユーザを作成できない | ? |
参照 | 〇: 作成・変更したユーザ名を参照できる | n/a: 作成・変更できないので対象外 | ? |
変更 | 〇: 一度も使用されていないユーザ名に変更できる | ×: 現在使われているユーザ名に変更できない | ? |
削除 | 〇: 作成・変更したユーザを削除できる | n/a: 作成・変更できないので対象外 | ? |
最初に示した仕様記述の曖昧な点が明らかになりました。ユーザ名の一意性を「ある時点では一意」と考えるのか、「過去・未来に渡って一意」と考えるのかは重要な決め事なので、仕様をクリアにするいいチャンスです。
ユーザ名の使い回しがトラブルになりうるようなケースでは「過去・未来に渡って一意」とするのが良いのでしょうが、ここでは「ある時点では一意」、つまり「使い回し可能」ケースで表を埋めてみます。
一度も使用されていないユーザ名 | 現在使用されているユーザ名 | 過去に使用されていたが今は使用されていないユーザ名 | |
---|---|---|---|
作成 | 〇: ユーザ作成時に、使用されていないユーザ名をつけられる | ×: 現在使われているユーザ名を指定したユーザを作成できない | 〇: ユーザ作成時に、過去に使用されていたが今は使用されていないユーザ名をつけられる |
参照 | 〇: 作成・変更したユーザ名を参照できる | n/a: 作成・変更できないので対象外 | 〇: 作成・変更したユーザ名を参照できる |
変更 | 〇: 使用されていないユーザ名に変更できる | ×: 現在使われているユーザ名に変更できない | 〇: 過去に使用されていたが今は使用されていないユーザ名に変更できる |
削除 | 〇: 作成・変更したユーザを削除できる | n/a: 作成・変更できないので対象外 | 〇: 作成・変更したユーザを削除できる |
ここでも、「過去に使っていた自分のユーザ名を、再度使う」というケースを考慮してもいいかもしれません。
テスト設計3: ユーザ名のライフサイクルを考える
さて、ユーザ名の「状態」を考えた以上、状態遷移も考えたくなります。ユーザ名が生成されてから消滅するまで、これはユーザ名の「ライフサイクル」ということができるでしょう。
状態は、以下の3つでした。
- 過去から現在まで一度も使用されていない
- 利用されている
- 過去に使用されていたが、現在は使用されていない
ユーザ名のライフサイクルには、以下の5つがあります。
- 初期状態→1→終了状態: 一度も使われることのなかったユーザ名
- 初期状態→1→2→終了状態: 一度だけ使われたユーザ名
- 初期状態→1→2→3→終了状態: 一度使われた後、使われなくなったユーザ名
- 初期状態→1→2(→3→2)n→終了状態: 使い回されたユーザ名
※nは繰り返しの数。n>0とする。n=0はサイクル2と同じ。 - 初期状態→1(→2→3)n→終了状態: 使い回された後、使われなくなったユーザ名
※nは繰り返しの数。n>1とする。n=0はサイクル1と、n=1はサイクル3と同じ。
上のCRUDとは別に、このライフサイクルのテストもしたくなります。といっても、内容は重複しています。
サイクルaはテストするまでもないでしょう。サイクルbとサイクルc、サイクルdのn=1もカバーされていると言えるでしょう。
サイクルdとeを区別したテストは必要なさそうですが、サイクルdのn>1、サイクルeのn>2の、「複数回の使い回し」が起こるパターンはどう考えればよいでしょう。
これはもう仕様と実装によるので唯一解はなさそうですが、一般的にはこういうテストを行いたくなります。
- n=3、つまり「使い回しが2回起こる」パターン。
- n=大きな数、つまり「使い回されまくる」パターン。優先度低。
言いたかったこと
「一意の値」パターンのテストについて、「これが鉄板でござい!」と言いたいわけでないです。
ある仕様に対してテストケースを思いついてたら、それをポイっと追加する前に、
- そのケースは、どういう観点の中に位置付けられてられるのか
- その観点には、他にどのようなケースがあるだろうか
- その観点を網羅するにはどうしたらいいだろうか
を考えるのがいいなあというのが、実は今回の主張だったりします。
単純な例ですが、「仕様で制限された文字数より長いテキストを入れたらどうなるんだろう」と思いついたら、
- これは「文字列長」というテスト観点に属するテストケースだな
- このテスト観点には、「短いテキスト」というテストケースもあるな
- じゃあ、何文字のテストをやれば、この「文字列長」という観点を網羅できるだろう。同値分割を使ってみるか。
みたいな感じです。
サマリ (再掲)
- ありがちな仕様なのに、テスト設計の定石がそこまで広まってないものがある気がする。
- 「一意な値」パターンは、CRUDとライフサイクルの2つを考えるとよさそう。
- テスト設計は、仕様の曖昧さを減らすことのできる活動である。早く始めよう。
- 思いついたケースを無造作に追加していくのではなく、そのケースからテスト観点を逆導出すると、網羅性が考えやすくなる。
今回「一意な値」パターンと名付けてみましたが、このように「ありがちな仕様」をパターンとして抽象化し、それに対するテスト設計の定石をまとめるって、もしかするとすごく面白そうじゃないですか?
もうそういうのありますかね?