業務用 Web アプリケーション開発のテスト自動化


ここでは Web 上で動作するシステムに対する自動テストの書き方をまとめたものを紹介します。一般的な業務用 Web アプリケーションを主軸として考えているため、業務 SE の人たちにも理解して貰えるように説明していきます。
テストツールなどのインストール方法や使用方法は省略しています。そちらについては参考になるような URL を記載しておきます。
最初は準備や考え方から初めて、次にテストの方法やコードを見ていきます。そして最後はまとめとして QA などを記載していきます。

対象となる読者

  • 自動テストがどんな仕組みなのかわからない
  • 自動テストを現場に導入したいけど、どうしたらいいのかわからない
  • 自動テストの書き方は知っているけど、どこから手を付けていいのかわからない

開発環境、テストツールなど

開発環境、言語、ライブラリ、実行環境

  • Visual Studio 2015
  • C#
  • ASP.NET Web Form
  • IE11

Windows 環境の業務アプリケーション開発で一般的に使用されるものを組み合わせています。Web システムのテストの考え方は言語やライブラリに依存しないものなので、今回使用する環境以外にも適用可能です。

テストツール

  • MSTest
  • Selenium WebDriver
  • Jasmine

テスト自動化に必要なものを組み合わせて一例を上げています。他にも色々なものがたくさんあるため、プロジェクトに合ったものを選択してください。

MsTestによるユニットテストの解説
C#でSelenium2を使用して主要ブラウザを動かしてみた
Jasmine使い方メモ

自動テストの仕組み

自動化するテストの種類は三つに分けることができ、ピラミッド形式で表現することができます。

多くのアプリケーションは典型的に三つの層で構成されるため、先程説明したテストのピラミッドと一致させることが可能です。

ASP.NET Web Form の開発では主に UI-BC-DAC の 3階層構成が使用されています。この構成の場合は BC に対してサービス層とロジック層がまとまってしまっているため、それぞれ住み分けが必要になります。住み分け方法はそれぞれのテストで紹介します。

3階層を簡単に説明してみよう

UIテスト

UI〜ロジックまで全ての層を通過するテストです。一連の流れのテストが可能な反面、壊れやすい、重いなどといったデメリットもあります。使用する場面は可能な限り少なくしたほうがよいです。
例えとしてログイン画面の UI テストは二つのケースが考えられます。

  • 正常にログインができたこと
  • エラーでログインができなかったこと

エラーケースは詳細を詰めていくと ID が入力されていないケースや、パスワードが間違っているケースなど色々なケースに分かれると思いますが、UI テストではまとめておいたほうがよいです。

[TestClass]
public class LoginTests
{
    private static IWebDriver _driverIe;
    private const string TestPageUrl = @"http://localhost:3000/login.aspx";

    [AssemblyInitialize]
    public static void SetUp(TestContext context)
    {
        _driverIe = new InternetExplorerDriver();
    }

    [TestMethod]
    public void SuccessTest()
    {
        _driverIe.Navigate().GoToUrl(TestPageUrl);
        _driverIe.FindElement(By.Id("UserId")).SendKeys("MyName");
        _driverIe.FindElement(By.Id("Password")).SendKeys("hogehoge1");
        _driverIe.FindElement(By.Id("AddButton")).SendKeys(Keys.Enter);
        var acctual = _driverIe.FindElement(By.Id("MessageLabel")).Text;
        Assert.AreEqual("Welcome MyName", acctual);
    }

    [TestMethod]
    public void ErrorTest()
    {
        _driverIe.Navigate().GoToUrl(TestPageUrl);
        _driverIe.FindElement(By.Id("UserId")).SendKeys("MyName");
        _driverIe.FindElement(By.Id("Password")).SendKeys("xxxxxxxx");
        _driverIe.FindElement(By.Id("AddButton")).SendKeys(Keys.Enter);
        var acctual = _driverIe.FindElement(By.Id("ErrorLabel")).Text;
        Assert.IsNotNull(acctual);
        Assert.AreEqual(TestPageUrl, _driverIe.Url);
    }
}

統合テスト

サービス層とロジック層の繋がりのテストです。これによって復数の層が繋がっていることを確認する機能を保ったまま、UI の壊れやすさにも影響されないテストが実施できるようになります。
下記などがテストの対象になり、HTTP リクエスト/レスポンスの内容を見ることによってテスト可能になります。

  • バックエンドの Web サービスや、API など
  • UI が存在しないアプリケーションのテスト

ASP.NET Web Form の統合テスト

ここからは ASP.NET Web Form 独自の仕様を説明します。ASP.NET Web Form ではイベントドリブンモデルを使用するために UI をコードビハインドで設計するように作られています。更に ViewState という仕組みを使って HTTP リクエストにページ情報を埋め込んでシステムを制御しています。
これによって統合テストで実施すべき HTTP リクエスト/レスポンスのテストが実装しづらくなっています。なので ASP.NET Web Form の 3階層アプリケーションで統合テストを実施する場合は、BC のテストを実施することで代用します。先に説明したとおり BC には統合テストとユニットテストの住み分けが必要になります。統合テストで実施されるテストは下記などがあります。

  • UI から情報を受け取り、処理を実施するクラスやメソッド
[TestClass]
public class LoginBcTests
{
    [TestMethod]
    public void LoginUserTest()
    {
        var target = new LoginBc();

        // Success
        Assert.IsTrue(target.LoginUser("MyName", "hogehoge1").IsLogin);

        // Error
        Assert.IsFalse(target.LoginUser("MyName", "xxxxxxxx").IsLogin);
    }
}

ユニットテスト

ロジック層のテストです。正確性、スピード、カバレッジ全てにおいて頼りになります。柔軟性が高く高速で動作するため、UI テスト、統合テストよりも優先すべきテストになります。UI テストで実施しなかったエラーケースの網羅はここで実施します。

ASP.NET Web Form のユニットテスト

3層アプリケーションの BC でユニットテストに分類されるテストは下記のようなものがあります。

  • UI から情報を受け取らず、他の BC から情報を受け取って処理を実施するクラスやメソッド
  • DAC クラス
  • ユーティリティクラス
  • コアクラス
[TestClass]
public class CopyTests
{
    [TestMethod]
    public void DeepCopyTest()
    {
        // Value type
        int value = 123;
        Assert.AreEqual(123, Utility.Copy.DeepCopy(value));

        // Reference type
        var model = new CopyTestsModels { Name = "Test Name" };
        var copyModel = Utility.Copy.DeepCopy(model);
        copyModel.Name = "New Name";
        Assert.AreNotEqual(model.Name, copyModel.Name);
    }
}

JavaScript を使ったブラウザ上のユニットテスト

JavaScript は一見 UI テストに該当すると思われがちですが、UI テストでは全ての層の繋がりをテストすることに対して、JavaScript のユニットテストではあくまでブラウザ上だけのテストを実施します。そうすることによって、ユニットテストのメリットを活かした自動テストが可能になります。

describe('add test', function() {
    it('1 + 1 = 2', function() {
        expect(add(1, 1)).toBe(2);
    });

    it('1 + 4 = 5', function() {
        expect(add(1, 4)).toBe(5);
    });
});

QA

Q. どこからテストを実装していけばいいの?
A. ユニットテストがおすすめです。その中でも特に色々な部分で使用しているユーティリティクラスや、重要なクラスは優先度が高いです。

Q. 業務上証跡が必要なんだけど、どうすればいいの?
A. テスト結果を証跡として残しましょう。テストライブラリは基本的に結果を出力できるようになっています。画面イメージが必要な場合は、UI テストの処理に画像を保存する処理を入れておけば問題ありません。

Q. 開発工数が多くなってしまいそうな気がするんだけど、どうすればいいの?
A. 作って納品して終わり。という開発なら工数は多くなると思います。メンテナンスすることがあるアプリケーションの場合は、リグレッションテストの部分で必ず自動テストは役に立ちます。品質向上にも一役買ってくれます。

参考書籍

O'Reilly Japan - 初めての自動テスト

もっとテストについて細かく知りたい人におすすめの書籍です。この記事を書こうと思ったきっかけをくれた書籍でもあります。考え方は基本的に同じなので、すんなり読み解けると思います。
自動テストに便利なモックやデザインパターンもわかりやすく触れているので、入門書として最適です。