$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

良いテストケースの作成手法を学ぶ - 「はじめて学ぶソフトウェアのテスト技法」を読んだ

ソフトウェアテストに関する知識をもう少し言語化したいなと思い、「はじめて学ぶソフトウェアのテスト技法」を読んだ。

この本では主に良いテストケースの作成手法について学べた。良いテストケースとは「最小の時間と労力でほとんどのエラーを検出する可能性がもっとも高くなるようなテストケース」のこと。これにできる限り近づけられるようにテストケースを工夫する。

良いテストケースを作るためにどういう技法があるかをこの本はいくつも教えてくれる。自分がこれまでテストを書いていると「こういうテストの方がなんとなくベターだよな...?」みたいに感覚的に考えていたところを、言葉として定義してくれることで構造化できるのはありがたかった。たとえば

  • 同値クラステスト
    • 同じグループのテストが、以下を満たせば同値クラスを形成する
      • 同じ機能をテスト
      • 1つのテストで障害が見つかれば、残りのテストでも見つかると予想できる
      • 1つのテストで障害が見つからなければ、残りのテストでも見つからないと予想できる
    • 同値クラスに含まれるケースはただ一つで良い
    • 無効な入力には1回に1つの無効値のテストをすれば良い
  • 境界値テスト
    • 境界で、何をテストすることで良いテストケースに近づけられるか
  • 状態遷移テスト
    • 状態遷移図は、テストすべき状態、イベント、アクション、遷移を明確にしてくれるので、テスト作業の指針として使われる
    • 理想的には状態遷移を作って、すべての遷移を少なくとも1回はテストするようなテストケースを作成するのが推奨

他にも「ほとんどの欠陥はシングルモード欠陥(ある機能が単に動かない)、ダブルモード欠陥(あるペアで動かない)のどちらか」という話は面白かった。組み合わせをテストするときに完璧に網羅しようとすると全組み合わせを作る必要があるがテストケースが多くなりすぎる、ダブルモード欠陥までがほとんどの欠陥なのでペアを網羅できれば十分みたいな話だった。なるほどなーという感じ。

こんな感じで面白かった一方で、テストをずっと書いてて感覚は身についているけどもっと言語化したいという欲求は満たせるが、テスト入門書としてはちょい微妙かなと思った。そういう本を選びたいときは別の本が良さそう。別で単体テストの考え方/使い方を読んだけど、そちらの方が分かりやすかったかも。

読書ノート

- この本が扱うのは、以前より少ないリソースで、ずっと多くの欠陥を検出できるテストケースを選択・作成できるようにすること 23
    - 最小の時間と労力でほとんどのエラーを検出する可能性が最も高くなるようにテストケースを設計する 33 🌟
- 同値クラステスト 44
    - 同じグループのテストが、以下を満たせば同値クラスを形成する 47
        - 同じ機能をテスト
        - 1つのテストで障害が見つかれば、残りのテストでも見つかると予想できる
        - 1つのテストで障害が見つからなければ、残りのテストでも見つからないと予想できる
    - 同値クラスに含まれるケースはただ一つで良い
    - 無効な入力には1回に1つの無効値のテストをすれば良い 58
- デシジョンテーブル
    - システムが実装しなければならない複雑なビジネスルールを記録するために用いられる 88
    - テストケースへの変換ができる 83
- ペア構成テスト 132
    - 変数値の全てのペアをテストする
    - ほとんどの欠陥はシングルモード欠陥(ある機能が単に動かない)、ダブルモード欠陥(あるペアで動かない)のどちらかなので、全てのペアだけで十分検出できる
- 状態遷移テスト 136
    - 状態遷移図は、テストすべき状態、イベント、アクション、遷移を明確にしてくれるので、テスト作業の指針として使われる156
    - 理想的には状態遷移を作って、すべての遷移を少なくとも1回はテストするようなテストケースを作成するのが推奨 149

妻が書いた「めんどくさがりやの自分の機嫌を取る暮らし」が発売されました

PRです。妻が書いた「めんどくさがりやの自分の機嫌を取る暮らし」が本日発売されました。

僕から見ても、妻はめんどくさがりやなのに「こんな生活がしたい!」という理想は高い性格に見えています。そういう性格を満たすために色々工夫をしているのですが、その工夫がコミックエッセイになりました。目次をピックアップすると、こういう話題を取り上げています。

  • 髪の毛のケアがめんどくさい…でも、ツヤツヤ髪になりたい!
  • 文字だけの本を読むのがむいてない…でも、いろんな知識を身に付けたい!
  • 果物の皮をむくのがめんどくさい…でも、フルーツたくさん食べたい!
  • お湯を沸かすのがめんどくさい…でも、お白湯生活始めたい!
  • 眉毛を描くのがめんどくさい…でも、似合う眉毛を手に入れたい!
  • 花を育てる才能がない…でも、花のある暮らしがしたい!
  • ネイルするのがめんどくさい…でも、セルフネイルしてみたい!
  • 買い物に行くのがめんどくさい…でも、できたてのご飯を食べたい!
  • 運動をするのがめんどくさい…でも、引き締まったボディがほしい!
  • DIYはめんどくさい…でも、理想が詰まった仕事部屋がほしい!


僕はこの中では、お白湯生活始めたい!」「できたてのご飯を食べたい!」「理想が詰まった仕事部屋がほしい!」あたりでやっている工夫によってQoLが上がったなと感じています。僕は食事と仕事が日々の生活の中で重要度が高いので、その辺りの質が上がるのはありがたい。

こんな感じで少しの工夫によってQoLが上がる様子が描かれているので、目次で共感できる部分があればぜひ買ってもらえると嬉しいです。

以下から試し読みもできます。 ebookjapan.yahoo.co.jp

Goでstruct内に定義されたタグの内容を再帰的に取得する

あるstruct構造の中のdbというタグの内容に何が含まれているかをすべて取得したい時があったため。

たとえばこういう構造があったとして

type Timestamp struct {
    CreatedAt time.Time `db:"created_at"`
    UpdatedAt time.Time `db:"updated_at"`
}
type JsonField struct {
    Foo string `json:"foo"`
    Bar string `json:"bar"`
}
type Post struct {
    ID       int64     `db:"id"`
    UserID   int64     `db:"user_id"`
    Body     string    `db:"body"`
    PostedAt time.Time `db:"posted_at"`
    Deleted  bool      `db:"deleted"`
    NoName   string    `db:"-"`
    JsonBody JsonField `db:"json_body"`
    Timestamp
}

dbタグに含まれる以下の文字列を全て取得したい。

id
user_id
body
posted_at
deleted
json_body
created_at
updated_at

reflectで頑張る

再帰的に取ってくるのをreflectを使って自分で頑張るとこんな感じ。

func main() {
    p := Post{}
    dbTags := extractDBTags(reflect.TypeOf(p))
    for _, tag := range dbTags {
        fmt.Println(tag)
    }
}

func extractDBTags(t reflect.Type) []string {
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }

    dbTags := make([]string, 0)

    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)

        // Get the `db` tag if any
        dbTag, ok := f.Tag.Lookup("db")
        if ok && dbTag != "-" {
            dbTags = append(dbTags, dbTag)
        }

        // If this is an embedded struct, recurse
        if f.Type.Kind() == reflect.Struct {
            dbTags = append(dbTags, extractDBTags(f.Type)...)
            continue
        }
    }

    return dbTags
}

jmoiron/sqlx/reflectx を使う

https://pkg.go.dev/github.com/jmoiron/sqlx/reflectx を使うとちょっと簡単に実装できる。

func main() {
    m := reflectx.NewMapper("db")
    fields := m.TypeMap(reflect.TypeOf(p))
    for tagname, _ := range fields.Names {
        if !strings.Contains(tagname, ".") {
            fmt.Println(tagname)
        }
    }
}

このやり方の場合、if !strings.Contains(tagname, ".") { の条件がないと、json_body.Fooみたいなのも入ってきてしまうのが難しいところ。もしかしたらもっといい方法あるかも。