TDDはゆるく実践しても大丈夫

 最近、TDDのテストコードは捨てても良いかみたいな議論を見ました。
 これに対する自分個人の経験上の意見ですが、TDDは雑多にテストコードを使い捨てても効果を出せると思います。

 もちろん、TDDで保守性が高く価値あるテストを書いて、捨てずにCIや中長期的なリファクタリングで再利用していくと、TDDの効果を増幅できます。ただ、それをするにはスキルや事前の工夫、労力が必要ですし、できる場面に限りがあります。
 そういったことをやらず、もっとゆるい姿勢で取り組んでも、費用対効果をプラスにできる手法がTDDだと考えています。

 今回は、そのTDDでゆるくしてもよいポイントを、実経験からまとめたいと思います。

TDDのテストは使い捨てでいい

 TDDのテストはプログラマのこまごまな課題に応じて累積的に作られるため、保守コストがかかるテスト・保守する価値の低いテストが生まれがちです。そのためテストの使い捨ての発生はむしろ自然な流れです。
 またTDDのテストはプログラマの不安を解消するといった軽い動機付けで作ってよいので、ゆるい動機でテストを書いて、ゆるい判断でテストを捨てていくスタイルでも、TDDは効果を出せます。

【発展的内容】もちろんTDDで再利用し続けられるテストを増やして、CIなどに組み込んでいけると、TDDの費用対効果を増幅できます。ただそれをするにはテストの保守性確保(テストコードの再整理等)、有効なテストを作るテスト設計スキル、テスタビリティの確保の労力・スキルが必要です。さらに適用できる場面にも限りがあります。そういったものは、個人でTDDをうまく回せるようになった次のステップから手掛けてみても大丈夫だと思います。またそれができるのは一部のみという認識も大事です。

TDDのテストの網羅性は気軽に主観で決めていい

 TDDでのテストの十分性・網羅性は、主観でざっくり判断してよいです。例えばプログラミング中に不安を感じたら、主観で不安がなくなったと感じるまでテストをかけばそれで十分、といった感じです。体系的なテスト手法やテスト技法で、ロジカルにテスト設計しなくても効果を出せます。
 逆になれない技法で厳格なテスト設計を行おうとすると、スピードが落ちて、TDDの費用対効果の費用が悪化しがちです。

【発展的内容】もちろん はじめて学ぶソフトウェアのテスト技法 で挙げられているような、体系的なテスト手法やテスト技法を身につけて、有効なテストを軽快に確保できるようになると、TDDでも効果的なテストコードがかけるようになります。ただこれもTDDを回せるようになった次のステップで、逐次身につければよいと思います。

TDDの適用は一部だけでいい

 あえていうまでもないかもしれませんが、TDDはコードの全てに適用できません。自動テストのテスタビリティを確保できる箇所のみで効果を発揮できます。テスタビリティを確保できない箇所でTDDを行うと失敗するか、費用対効果がマイナスになる可能性が高いです。
 そのためTDDがやりにくいと感じたらTDDをやめても大丈夫です。プログラミングの2割ぐらいしかTDDができなくても、その2割で効果を出せれば御の字です。

【発展的内容】なおTDDの適用範囲を広げるためには、アーキテクチャレベルでの自動テストのテスタビリティの作り込みが必要です(ユニットレベルのテスタビリティはTDDで確保できます。それだけでは解決できない課題に対応するため、アーキテクチャレベルの工夫が必要になります)。これもTDDをまわせるようになった次のステップで挑戦するとよいと思います。

テストはあとから書いてもいい

 テスト駆動・TDDとは呼べませんが、テストはあとから書いても大丈夫です。
 というのも、TDD実践のとても重要な前提に、「自分の手足のように軽快にユニットテストを活用して、プログラミングを効率化する習慣の実現」があるためです。例えば、仕様に迷ったらテストファーストでテストをサッと書く、書いたコードに不安を感じたらテストをさっと継ぎ足す、のような状態です。
 テストファーストであれ後付けであれ、とにかく気軽にユニットテストを活用する習慣を続ければ、その前提が満たされ、TDDを実践できるタイミングが一気に増えます。
 なのであとからテストを書くのに気後れは不要です。気軽に・軽快にユニットテストをプログラミングに活用することが大事です。

ユニットへのテストでなくてもいい

 パッケージや、アーキテクチャレベルのレイヤといった大きな粒度の方が、有効・堅牢な自動テストを書きやすい場面は少なくありません。そういう場合は、大きな粒度のテストでテスト駆動開発を行っても大丈夫です(いわゆる 実践テスト駆動開発 のOutside-In TDDやTDDのダブルループはその代表例です)。「TDDのテストはユニット(コードの最小単位)ごとに書かなければならない」という原理主義に拘る必要はなく、書きやすい粒度のテストで駆動させるのが費用対効果改善に有効です。

【発展的内容】これをやりやすくするためには、アーキテクチャレベルでのパッケージやレイヤ、バウンダリの粒度で、堅牢で自動テストを書きやすいインターフェースを確保するのが重要です。そこで確保したインターフェースに対する自動テストでテスト駆動を行うことで、TDDのテストの堅牢性、有効性を高められます。そうすればTDDのテストをCIなどで再利用できる場面が増えます。

TDDはチーム全体でやらなくてもいい

 TDDは一人で実践しても費用対効果をプラスにできます。

 プログラミングをしていれば、コードの仕様を検討したくなったり、不具合のリスクを感じたり、リファクタリングしたくなったりする場面に数多く遭遇すると思います。それらを個人レベルで気軽に解決するのがTDDです。

 逆に個人では費用対効果がマイナスで、チーム全体でTDDしないと費用対効果を出せない、みたいな状況だと、TDDのやり方に問題がある可能性が高いです(これはチーム全体のスピード不足か、テスタビリティ実装スキル不足が原因であることが多いです。あるいは前述したゆるさを許さず完璧なTDD実践をしようとしているかもしれません)。

【発展的内容】ただテストをCIなどで継続的に利用するためには、チームでの推進が重要です。その実現には技術リードや開発インフラ整備の主導が求められます。これもTDDを個人で回せるようにしてから、次のステップで挑戦するとよいと思います。

余談:使い捨てを減らし、TDDのテストの費用対効果を高めるために

 TDDは、プログラマが個人主観で気軽にテスト書いては捨てるアプローチでも効果が出せます。
 ただし、次のステップとして、TDDで以下の実現を助ける(※)と、テストの使い捨ても減り、費用対効果をさらに高められます。

  • 堅牢なテストを作り、それを継続的に利用して、リグレッションバグ検出やリファクタリングに活用する
  • 有効なテストを作り、効率的にバグを見つけたり品質を保証したりする

※TDDで上記を直接担保するのではなく、TDDのテストを流用して上記を助ける形です。


 個人活動にフォーカスを絞った場合、この次のステップに進むためには次のスキルが求められます。

  • 自動テストのテスタビリティを確保するスキル
    • アーキテクチャ設計の工夫が求められます。テスト自動化の支障となる要素は局所化・分離する、各パッケージを切り出して自動テストできるようにする、ユニットテストを広く実行できるような設計方針を建てると言ったものです。
  • テストの保守性を作り込むスキル
    • テスト対象の変更に強くする(変更性を高める)、テストコードの結合性・凝集性を改善する、テストコードの可読性を高めて保守しやすくする、といったものです。
  • 有効なテスト設計を行うスキル。
    • 必要なテストを見極め、そのテストの要求に対して、妥当なテスト設計を行うものです。リスクベースドテストや、有効・網羅的なテストを作るための各種技法の使いこなしが求められます。

 このステップに進むためには、TDDの活動に、品質保証の戦略立て(TDDのテストを再利用してどのような責務を果たすべきかのテスト要求を明らかにする)、テストコードの改善・リファクタリング、テスト設計の改善、Outside-In TDDのような粒度の大きなテストサイクルの構築の活動の補強が求められます。

 そしてテストの再利用を行うためには、チームレベルでの推進もある程度必要です。CIや共通のテスト自動化環境を整備し、チームでテストを継続的に改善・保守していく習慣が求められてきます。

テスト観点とは

 テスト設計で「テスト観点」という言葉がよく使われるのですが、意味が広く、議論の発散の要因になりがちと感じています。そこで、今回は「テスト観点」の用語について、前提の「観点」の用語とともに簡単に解説したいと思います(いろいろな定義付けがされている言葉なので、あくまで筆者の見解になります)。

観点とは

 まず「観点」という用語ですが、これは「着目したい特質・側面の方向性」という意味付けの言葉です。
 例えば椅子について考える場合ですと、「上から見たらどう観えるか」「前から見たらどう観えるか」といったものが観点になります。また空間座標的なものに限らず、次のようなものも観点に該当します。

  • 重さはどうか、材質はどうか、耐用年数はどうか
  • ●●の規格に適合しているか
  • ●●のレビューサイトでの評価はどうか

 このように観点には多種多様なものがあります。

 なお観点を愚直に表現すると長くなるので、しばしば観点は単語に省略表記されます。例えば「重さはどうか」「材質はどうか」「耐用年数はどうか」といった観点は、観点のモデリングなどでは「重さ」「材質」「耐用年数」と省略表記される場合がよくあります。

観点の用途

 観点の用途として、主に以下の2つがあります。

  1. 対象の側面・特質を観察・分析するために使われる
    • 例えば、ソフトウェアを作ろうとする際に、作りたいソフトウェアを表現するために、複数の観点を用います。
  2. 側面・特質の観方を明示するために使われる
    • 例えば、テストエンジニアのテスト設計のノウハウを明示化する際に、対象者の特質を明示するため、対象者の観点を抽出します。

観点の導き方

 観点は、目的制約に沿って導かれ、使用されます。

目的に基づいた観点の選択

 まず観点は目的に沿って選ばれます。
 例えば「大きな椅子を部屋に運び込めるか明らかにする」という目的があるとします。
 この目的を詳細化すると例えば「椅子がドアの枠内に入るか明らかにする」という目的が出てきます。するとその目的達成手段の検討のために、「椅子の高さはどれぐらいか」「椅子の横幅はどれぐらいか」という観点が導かれます。
 また詳細化で「椅子を手で運べるか明らかにする」という目的が出てきます。するとその目的達成手段の検討のために、「椅子の重さはどれぐらいか」という観点が導かれます。

 このように目的達成の手段の検討を支えるため、観点が導出されます。

制約に基づいた観点の選択

 一方、観点は制約に見合うように選択・整理されます。

 ここでいう制約の代表的なものとして、時間・人・コストなどのリソース制約があります。観点は無数にありますが、リソースは有限です。リソースに見合うように、観点を整理・絞り込む必要があります。
 また観察者の限界もあります。観点が増えすぎると、人間が認知できる限界を超え、観察や分析が困難になります。すなわち観察者が扱えるまで、観点を整理・絞り込む必要があります。
 「整理・絞り込み」には、次のアプローチが使われます。

  • 必要性の低い観点を捨てる
  • 抽象化する
  • 関心ごとの分離を行って、分けて考えられるようにする
  • 構造化などの整理を行って扱いやすくする

観点を組み合わせる

 一定以上の複雑さを持つ目的に対応する場合、多数の観点の組み合わせが求められます。
 例えば「立体の椅子の形状を把握する」ことを目的とする場合、正面から見る観点だけでは目的を果たせません。前、上、下、右、左、後の6面と、複数の観点を組み合わせる必要があります。
 より複雑な目的に対応しようとすると、必要な観点は更に増えます。例えば「椅子を製造する」という目的ならば、立体形状だけでなく、重さ、材質、加工方法、コスト、材料の納入見込み、耐用年数、製品の品質のばらつきといった観点の検討も必要になります。
 こうした観点の爆発に対しては、観点の抽出過程の段階でも、最終的な観点の成果物表現の段階でも、前述の削減・抽象化・分割・整理が求められてきます。

観点を導くためのインプット

 観点は様々なインプットを使って導き出されます。

 まず観る対象の特質を抽出し、そこから観点をピックアップするアプローチがあります。例えば後述するテスト観点については、テスト対象を記載したテストベースから情報を分析し、テスト設計の観点に反映する場合があります。

 また外部から観点を持ってくるアプローチもあります。
 よくあるのが観る者の知見に基づいた観点です。「過去の失敗経験に基づいて失敗リスクの高い特質に着眼する」「長年学んできた膨大な知見に基づいて、有効な観点を総合的にピックアップする」「作業している中で感じた気がかりを観点にする」といったアプローチは普遍的に採用されます。
 また、外部の形式知を使うアプローチも一般的です。形式知とは、グッドプラクティス、パターン、標準といったものです。ソフトウェア開発ですと、仕様から設計構造を見出すためのGRASPパターンや、ISO25010の品質特性といったものが該当します。なお外部の形式知から観点を導入する際は、対象に合わせた取捨選択やテーラリングが求められるのが一般的です。

テスト観点とは

 次に本題のテスト観点についてです。
 テスト観点は、テストの活動で使われる観点です。

 様々なテストの活動に対応して、様々なバリエーションがあります。例えば次のようなものがあります。

  • テストの計画や戦略を検討するための観点(観点の対象:テストの計画や戦略)
  • テストのニーズ・シーズからテストの要求を分析するための観点(観点の対象:定義されたテスト要求)
  • テストベースから必要なテストを見つけ出すための観点(観点の対象:テストケース)

 例えば「テストベースから必要なテストを導くための観点」ですと、具体例としては以下のようなものが該当します。

  • ログイン機能のふるまいと仕様書の記載の合致性のテストは必要ないか?
  • 通信過負荷時の動作の確認は必要ないか?
  • APIの処理速度のテストは必要か?

 どれも「テスト観点」と省略されて語られる場合があります。会話の混乱のもとになりがちなので、具体的な意味付けを行いたい場合は、「テスト観点」が具体的にどの観点について語っているか明確化した方が無難です。

一般的なテスト観点の用例

 なお一般的にテスト観点というと、「テストのインプット(テストベースなど)から、テストケースを導くために用いる、テストすべき特質や側面」を指す場合が多いです。この観点は、以下の2つに分類されます。

  • 目的(What)から目的達成手段(HowTo)への転換の切り口(例えば、要求を実装へ転換するための取っ掛かり)
  • 抽象的・曖昧なテストケースから具体的なテストケースへ設計する手段

 前者は、テスト分析の切り口です。Ostrandの4つのビュー、企業が公開しているテスト観点表など、形式知化されている観点が代表例になります。後者も包含します。
 後者は、いわばテストの意図です。ここではテスト観点とテストケースが全射の関係を持ちます。十分にブレークダウンされたテスト観点はテスト条件として扱えます。

2軸4象限でフロントローディングの方針立てを整理する

 テストクラスタフロントローディングがよく話題になるようになっています。これに関して、作業や課題を2軸4象限で整理すると検討を整理できる場合があります。チームで課題を話し合うときの議論の整理といった、簡易的・アドホックな使い方限定のプチフレームワークですが、今回その整理方法について触れたいと思います。

2軸4象限

 整理に用いる2軸は次の通りです。

  1. 早期にやるべきかの必要性(あるいは重要性):
    • 早期にやるべきか、早期にやらなくてよいか(後回ししてもよいか)
  2. 早期にできるかの実現性:
    • 早期にできるか、早期にできないか

 これを図にして、次のような4象限マトリクスにします。それぞれA、B、C、Dの象限を割り当てます。

f:id:goyoki:20190922013034p:plain

※一般的な必要性(or重要性)と実現性のマトリクス分析を、フロントローディングに適用したものになります。

 おおよそですが、それぞれの象限ごとに、フロントローディングを進めるにあたっての対応方針が異なってきます。

4象限ごとのフロントローディングの対応方針

A、B、C、Dに対し、フロントローディングを進めるにあたっては次のような方針立てを行います。

「D:後回しにしてよい&早期からできる」の対応方針

  • Dの作業の例
    • 後のプログラミングで検討しても支障のない、詳細なIF仕様の設計など
  • Dに対する方針
    • 該当作業は、重要度と余裕に応じて実施します。後回しにするなら、いつ実施するか方針立てします。
    • このDに属する作業については、早めに処理する方針を立てれば、後の余裕を確保できます。ただやり直しや陳腐化のリスクの点で、B象限とA象限対応を優先する方が無難な場合があります。

「C:後回しにしてよい&早期からできない」の対応方針

  • Cの作業の例
    • 顧客リリース物のチェックなど
  • Cに対する方針
    • 後回しで実施します。Cに属する作業については、後々実施すべきタイミングで確実に実施できるように、中期的な方針立てを実施します。

「B:早期にやるべき&早期からできる」の対応方針

  • Bの作業の例
    • プロジェクト成否へのインパクトの大きいリソース確保の調達計画など
  • Bに対する方針
    • 重要度に応じて、実施します。Bに属する作業については、該当する作業をきちんと識別して確実に実施する方針を立てるのが重要です。

「A:早期にやるべき&早期からできない」の対応方針

  • Aの作業の例
  • Aに対する方針
    • フロントローディングにあたって、知恵を絞って方針を工夫する対象です。3つの方針で対応していきます。
      1. 早期にできるようにして、B象限に移動させる
        • e.g. 未知未経験で早期に実施できない → 反復開発やプロトタイプで早期に実践経験を積んで、未知未経験を早期に解消する
      2. 後回しできるようにして、C象限に移動させる
        • e.g. 要求が未確定なために早期に実施できない場合 → 想定される要求を数パターン設定し、それらに対応できるように、外部インターフェースに可変点を設ける
      3. ステークホルダと調整してスコープ外にする

 全体の対応方針ですが、フロントローディングの推進ではA象限、B象限への注力が重要になります。逆にC、D象限の前倒しで時間を浪費して、本質的なA、B象限がおざなりになることも多いです。

アーキテクチャ設計での例

 フレームワークの使い方は主に次の2パターンです。

  1. 識別した作業の方針立て
    • 課題や作業を識別した後、それらがこの4象限のどこに分類されるのか考えることで、方針を考えます。
  2. 課題や作業を分析するための観点
    • 4象限を観点にして、課題や作業を分析します。

 アーキテクチャ設計を例にとると、チームメンバーで、アーキテクチャドライバを観点にして課題を出し、課題を重要度と4象限で分類して、フロントローディングの対応方針を立てるといった使い方をします。

Jenkinsfileを構造的に整理する

 Jenkins PipelineのJenkinsfileでテスト並列実行を最適化する - 千里霧中 つながりの話です。Jenkinsfileでは、groovyの構文を活用してJenkins Pipelineの実装を整理できます。

処理をメソッドとして抜き出す

 まずJenkinsfileではメソッドやクラスを任意に定義可能です。これにより重複する記述をメソッドにまとめるといったことが可能になります。
 例えば以下のJenkinsfileを例とします。

pipeline { 
  agent any 
  stages {
    stage('Test') { 
      steps {
        parallel 'run_test':{
          node('node1') {
            println 'run test code'
          }
          node('node2') {
            println 'run test code'
          }
        }
      }
    }
  }
}

 仮に「println 'run test code'」が長大な重複コードとすると、Jenkinsfileでは次のようにメソッドrun_test_code()としてまとめられます。

def run_test_code() {
  println 'run test code'
}

pipeline { 
  agent any 
  stages {
    stage('Test') { 
      steps {
        parallel 'run_test':{
          node('node1') {
            run_test_code()
          }
          node('node2') {
            run_test_code()
          }
        }
      }
    }
  }
}

Pipeline定義を動的に生成する

 Pipelineの定義はマップとして動的に生成することも可能です。ループ処理で定義の繰り返しを生成したり、条件分岐で定義を切り替えたりすることで、複雑なPipeline定義をシンプルなコードで実装できる場合があります。

 例えば以下のような重複の多いJenkinsfileを例に取ります。

//node1、node2でテスト並列実行
parallel 'node1' :{
  node('node1') {
    println 'run test code'
  }
, 'node2' :{
  node('node2') :{
    println 'run test code'
  }
}

これはfor文で以下のように重複部分をまとめられます。実行時の処理は前と同じです。

//node1、node2で並列実行
List node_list = ['node1', 'node2']
Map branches = [:]

for (int i = 0; i < node_list.size(); i++) {
  String node_name = node_list[i]
  branches[node_name] = {
    node(node_name) {
      println 'run test code'
    }
  }
}
parallel(branches)

ファイルを分ける

 Jenkinsfileは他ファイルのGroovyコードを呼び出せます。これにより、複数のJenkisfileの共通部分を外部モジュールとして切り出す、長大なJenkinsfileをファイル分割する、外部のライブラリを利用する、といったことが可能になります。

 例えば標準のJSONライブラリを使う場合は、次のような通常のimport構文で実装できます。

import groovy.json.JsonOutput
pipeline {
  agent any 
  stages {
    stage('Test') { 
      steps {
        parallel 'run test':{
          node('node1') {
            def hoge = JsonOutput.toJson([text : "dummy"])
            ...
          }
        }
      }
    }
  }
}

Jenkins PipelineのJenkinsfileでテスト並列実行を最適化する

 Jenkinsのpipelineを使うと、Jenkinsfileとしてジョブの定義・連携をGroovyで柔軟に記述できるようになります。そこでは例えば「変数を用いてノードやステージを横断する連携を実装する」「いろいろな外部スクリプトを実行して最適な実行条件を分析する」といった処理を容易に実装できます。

 今回はその具体例として、pytestのテスト実行時間の最適化を行うJenkinsfileを題材にします。

pytestのテスト並列実行の時間を最適化する

 対象は、pytestの2並列実行のジョブです。具体的には、Jenkins Pipelineで指定されたgitリポジトリから必要なファイルをチェックアウトし、その中のpytestのテストケースをノードサーバで実行して、結果をプッシュ& Jenkins Test Report出力する処理とします。実現にあたっては、(仮にテストケースが長大であるとして)ノードサーバを2つ用意し、テストを2分割してそれぞれのノードで並列実行させることで、テスト実行時間の短縮を狙います。

 このテストの2分割ですが、旧来の静的なジョブ定義では、テストケース数などで事前に静的分割せざるを得ない場合が多いです。一方でJenkins Pipelineを用いると、テスト実行時間が均等に分割されるように、テストケースを動的に2分割する処理を容易に実装できます。

Jenkinsfileの実装

 そのJenkinsfileの具体例を示します。ここでは以下を実装しています。

  • Prepareステージで、create_test_selection_file.py(実装例は後述)をシェル実行し、その標準出力をtest_selectionに格納します。
    • create_test_selection_file.pyは、テストケースファイルを、テスト実行時間が均等に2分割になるように2つにグルーピングし、結果をpytestの引数形式で標準出力するスクリプトです。過去のJUnit XML形式レポートからファイル名と実行時間のデータを抽出・集計することで実現します。
  • Testステージで、2つのノード:node1、node2にて、pytestのテストケースを並行実行します。pytestには、前ステージで得たtest_selectionを引数として指定します。
  • Reportステージで、pytestが生成したテストレポートファイルのプッシュと、Jenkins Test Reportへの出力を行います。

まとめると外に用意した集計スクリプトを呼び出して、ノードで実行するテストケースを動的に選択しています。

Jenkinsfileの実装例:

pipeline { 
    environment {
        test_selection = ""
    }
    agent any 
    stages {
        stage('Prepare') {
            steps {
                script {
                    def output = sh (
                        script: 'python create_test_selection_file.py',
                        returnStdout: true
                    ).trim()
                    test_selection = output.split(',')
                }
            }
        }
        stage('Test') { 
            steps {
                parallel 'automation test':{
                    node('node1') {
                        checkout scm
                        sh "pytest ${test_selection[0]} --junitxml=testresult/result1.xml"
                        sh "python push_test_report.py testresult"
                    }
                    node('node2') {
                        checkout scm
                        sh "pytest ${test_selection[1]} --junitxml=testresult/result2.xml"
                        sh "python push_test_report.py testresult"
                    }
                }
            }
        }
        stage('Report'){
            steps {
                git branch: 'main', url: 'git://hoge/integration-test.git'
            }
            post {
                always {
                    junit 'intagration_test/testresult/*.xml'
                }
            }
        }
    }
}
create_test_selection_file.pyの実装

 (主題ではないため簡易的なサンプルですが)例えば以下のようなものになります。

import glob
import xml.etree.ElementTree as ET
import numpy as np

NUMBER_OF_TESTNODE = 2

def select_test_by_time(test_files, resultfiles):
    test_result = {}
    all_time = 0.0
    for file in resultfiles:
        root = ET.parse(file).getroot()
        all_time += float(root.attrib['time'])
        for testcase in root.iter('testcase'):
            test_result[testcase.attrib['file']] = test_result.get(testcase.attrib['file'], 0) + float(testcase.attrib['time'])

    testsuite_time = 0.0
    node_no = 0
    output = ""
    for key, value in test_result.items():
        testsuite_time += value
        output += key + ' '
        if testsuite_time > all_time / NUMBER_OF_TESTNODE and node_no < NUMBER_OF_TESTNODE - 1:
            testsuite_time = 0
            output += ","
            node_no += 1
    output = output.replace('\\','/')
    print(output)

def select_test_by_number(test_files):
    test_group = list(np.array_split(test_files, NUMBER_OF_TESTNODE))
    output = ''
    for tests in test_group:
        output += ' '.join(tests) + ','
    output = output.replace('\\','/')
    print(output)

def create_test_selection_file():
    test_file = 'test/test*.py'
    input_file = 'testresult/*.xml'

    test_files = glob.glob(test_file)
    resultfiles = glob.glob(input_file)

    if resultfiles:
        select_test_by_time(test_files, resultfiles)
    else:
        select_test_by_number(glob.glob(test_file))

if __name__ == '__main__':
    create_test_selection_file()

atestcovでオールペア法での冗長なテストケースを見つける

ソフトウェアテスト #2 Advent Calendar 2018 - Qiitaの12/15の記事になります。

ATestCovの使い方の一例紹介です。

組み合わせテストツールATestCovを公開 - 千里霧中

atestcovを使って、テストケース削除時のnワイズカバレッジの変化を確認することで、nワイズテストにおける冗長なテストケースを検出できます。

pythonでのこの実装例を示します。
例は2ワイズカバレッジ(オールペア法)が対象です。テストケースを1つ削除した入力ファイルを全パターン生成し、atestcovで2ワイズカバレッジの変化を確認しています。
これにより、削除しても2ワイズカバレッジを下げないテストケースをピックアップします。

# check_2wise_testcase.py
'''指定されたテストケースのうち、削除しても2ワイズカバレッジを低下させないテストケースを検出する'''
import subprocess
import sys
import os
import re

def get_2wisecov(testcase_file, parameter_file):
    '''atestcovを実行し、2ワイズカバレッジ結果部分を抜き出す'''
    result = subprocess.check_output(f'atestcov.exe -t {testcase_file} -p {parameter_file}')
    return re.search('2wise coverage: ([0-9.]+)', result.decode('utf-8')).group(1)

def check_test(testcase_file, parameter_file):
    '''削除しても2ワイズカバレッジが変化しないテストケースをピックアップする'''
    origin_cov = get_2wisecov(testcase_file, parameter_file)
    print(f'2wiseカバレッジ:{origin_cov}%')

    with open(testcase_file) as f:
        lines = f.readlines()

    filename = 'temp.txt'
    for i in range(1, len(lines)):
        with open(filename, mode='w') as f:
            for index, line in enumerate(lines):
                if index == i:
                    remove_testcase = line.replace('\n', '')
                    continue
                f.write(lines[index])
        if get_2wisecov(filename, parameter_file) == origin_cov:
            print(f'-冗長なテストケース:{remove_testcase}({i+1}行目)')

        os.remove(filename)

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print("実行引数:python check_2wise_testcase.py テストケースファイルパス パラメータファイルパス")
        sys.exit(1)
    check_test(sys.argv[1], sys.argv[2])

実行例として、次の入力ファイルを用意します。

テストケース定義ファイル:SampleTestCase.txt

麺,スープ
細麺,豚骨
太麺,豚骨
細麺,醤油
太麺,醤油
太麺,豚骨

パラメータ定義ファイル:SampleParameter.txt

麺:細麺,太麺
スープ:豚骨,醤油

実行コマンドは次の通りです。

python check_2wise_testcase.py SampleTestCase.txt SampleParameter.txt

すると次のような結果が得られます。

2wiseカバレッジ:100.00%
-冗長なテストケース:太麺,豚骨(3行目)
-冗長なテストケース:太麺,豚骨(6行目)

これにより、2wiseカバレッジを基準とした場合、テストケース定義ファイルの3行目あるいは6行目のテストケースが冗長なのが分かります。

ATestCovを使って、テストケースで網羅できていないパラメータ組み合わせを見つける

先日公開したATestCovの使い方の一例紹介です。

組み合わせテストツールATestCovを公開 - 千里霧中

ATestCovでは、実行時引数「--info」(-i)の付与で、テストケースで網羅できていないパラメータ組み合わせを検出できます。
実行例として、以下のファイルを入力して実行します。

SampleParameter.txt(パラメータ入力ファイル)
#パラメータと値の一覧
麺:細麺,太麺
スープ:豚骨,醤油,味噌
SampleTestCase.txt(テストケース入力ファイル)
#テストケース一覧
麺,スープ
細麺,豚骨
太麺,醤油
細麺,醤油

今回、この入力ファイルを用いて、テストケースで網羅できていない、2つのパラメータの組み合わせを調べます。
調べ方は、2ワイズカバレッジ(2因子間網羅率)計測コマンドに、「--info」を付与します。実行コマンドは次のようになります。

./atestcov --testcase SampleTestCase.txt --param SampleParameter.txt --upper 2 --lower 2 --info

すると、2ワイズカバレッジに加えて、次のように「[info]」ログが出力されます。

atestcov(ver.0.04) ***
[info]measurering:2wise coverage
 [info]uncover:麺:細麺  スープ:味噌  
 [info]uncover:麺:太麺  スープ:豚骨  
 [info]uncover:麺:太麺  スープ:味噌  

[coverage report]
number of test case: 3
number of parameter: 2
2wise coverage: 50.00%(3/6)

このうち「[info]uncover:」の行で表示された組み合わせが、テストケースで網羅できていない組み合わせです。例えば「細麺の味噌スープ」を、テストケースが網羅できていないと読み取れます。

特定の組み合わせを調査対象から外す

網羅できていない組み合わせをピックアップする際、特定の組み合わせはピックアップから除外したい場合があります(例えば禁則の組み合わせなど)。その際は排他制約の文をパラメータに追加します。

例えば「太麺の豚骨スープ」は網羅しなくてよいならば、次のように@mutexで明記します。

SampleParameter.txt(パラメータ入力ファイル)
#パラメータと値の一覧
麺:細麺,太麺
スープ:豚骨,醤油,味噌
#禁則
@mutex 麺:太麺, スープ:豚骨


すると、出力で「[info]uncover:麺:太麺 スープ:豚骨 」が表示されなくなります。