Skip to content.

Sections
Personal tools
You are here: Home » コミュニティ » masarl memorial » masarl.cocolog-nifty.com » main » 2004 » 04 » Cotton Bolls: DBのテスト

Cotton Bolls: DBのテスト

Document Actions

« NUnit Converter | トップページ | Compositeパターンによる入力チェック »

2004.04.11

DBのテスト

ここ数年RDBを使う仕事がほとんどなかったが,今回のシステムでSQL Serverを使うことになった.テストコードを書くために以前からOracleで使っていた方法があったのだが,SQL Serverでもちゃんと使えるようなのでひとまず安心した.

このテストのポイントは,2つのDBユーザを使い分けるということだ:

  • 本番・受け入れテスト用ユーザ(マスターユーザ)
  • 単体テスト用ユーザ(テストユーザ)

つまり,画面から実行するときはマスターユーザ,JUnitやNUnitから実行するときはテストユーザでデータベースにログインする.こうすることで,テーブルの内容を壊さないテストコードを書くことが可能だ.

具体的には次のようにすればいい.ここでマスターユーザをmaster,テストユーザをtest,単体テストで更新されうるテーブルをtableAとする:

  1. testユーザでデータベースにログイン
  2. master.tableAのコピーをtest.tableAとして新規作成
  3. テストを実行
  4. test.tableAを削除

この方法は,testユーザから見た場合tableA がmaster.tableAではなくtest.tableAになることを利用している.例えば実装コードの中に

insert into tableA ( field1, field2 ) values ( 1000, 'ABC' )

があったとしてもmaster.tableAではなくtest.tableAにインサートすることになる.だから,tableAをいくら更新しても master.tableAの内容はそのままだ.

テーブルのコピーを作成するSQL文はOracleとSQL Serverで異なる.Oracleの場合は

create table tableA as select * from master.tableA where 1 = 0

だが,SQL Serverの場合は次のようになる:

select into tableA from master.tableA where 1 = 0

ここでSQL文に where 1 = 0 があるのは空のテーブルでテストしたいからだ.そうする必要がない場合はwhere句がなくてもいい.

以上をふまえてTableSaverクラスを作る.これは,テストコードでテーブルを上書きされないようにするためのクラスだ:

public class TableSaver
{
    private string userName;
    private string masterName;
    private SqlConnection con;
    private IList tableNames = new ArrayList();

    public TableSaver(string userName, string masterName, SqlConnection con)
    {
        this.userName = userName;
        this.masterName = masterName;
        this.con = con;
    }
    public void AddTable(string tableName)
    {
        string commandText
            = String.Format("select * into {0}.{2} from {1}.{2} where 1 = 0", 
                            userName, masterName, tableName);
        SqlCommand cmd = new SqlCommand(commandText, con);
        cmd.ExecuteNonQuery();

        tableNames.Add(tableName);
    }
    public void Restore()
    {
        for (int i = tableNames.Count - 1; i >= 0; --i)
        {
            string tableName = (string) tableNames[i];
            string commandText 
              = String.Format("drop table {0}.{1}", userName, tableName);
            SqlCommand cmd = new SqlCommand(commandText, con);
            cmd.ExecuteNonQuery();
        }
    }
}

TableSaverの使い方は次の通りだ.

TableSaver saver = new TableSaver("test", "master", con);
try 
{
    saver.AddTable("tableA");
    saver.AddTable("tableB");

    // テストの実行
    ...
}
finally
{
    saver.Restore();
}

ここでTableSaverの作成はsetUpメソッド,TableSaver.Restore呼び出しはtearDownメソッドで行うことになるだろう.しかし,実際には個々のテストケースからTableSaverを直接呼び出すことはない.CommonTestCaseの方法を使うためだ.ここでは,DBテスト用CommonTestCaseとしてDbTestCaseを作る:

public class DbTestCase : CommonTestCase
{
    protected SqlConnection con;
    private const string CONNECTION_STR = "Data Source=...";
    private TableSaver tableSaver;

    private void CloseConnection() 
    {
        con.Close();
    }
    private void RestoreTables()
    {
        tableSaver.Restore();
    }
    public override void SetUp()
    {
        base.SetUp();
        converter = new SqlSelectConverter(converter);
        con = new SqlConnection(CONNECTION_STR);
        con.Open();

        tableSaver = new TableSaver("test", "master", con);
        TearDownHandlers += new TearDownHandler(RestoreTables);
        TearDownHandlers += new TearDownHandler(CloseConnection);
    }
    protected void SaveTable(string tableName)
    {
        tableSaver.AddTable(tableName);
    }
}

ここでTearDownHandlersはC#版TestSaverだと思ってもらえればいい.C#にはdelegateの仕組みがあるのでTestSaverインターフェイスをimplementsしたクラスを作る必要がない.

DbTestCaseを継承した個々のテストケースでは,SaveTableメソッドを呼び出してテーブルを保護する:

[Test]
public void Sample()
{
    SaveTable("tableA");
    SaveTable("tableB");
    ...
}

また,DbTestCaseにはSQL文を簡単に実行できるヘルパーメソッドも用意しておく:

protected void ExecuteSql(string commandText)
{
    SqlCommand cmd = new SqlCommand(commandText, con);
    cmd.ExecuteNonQuery();
}

このメソッドはSQL文を実行するメソッドだ.C#にはヒアドキュメントの機能があるため,このメソッドだけでもかなり使える.

[Test]
public void Sample()
{
    ExecuteSql(@"
insert into tableA
(field1, field2) values 
(1000, 'ABC');
insert into tableB
(field1, field2) values 
(2000, 'XYZ');
");
    ...
}

長いSQL文が必要な場合はヒアドキュメントではなくファイルに書き込んで実行すればいい.これを実現するためのヘルパーメソッドは次の通り:

protected void ExecuteSqlFile(string fileName)
{
    string resourceName = GetType().FullName + "." + fileName;

    Assembly assembly = GetType().Assembly;
    Stream input = assembly.GetManifestResourceStream(resourceName);
    TextReader reader = new StreamReader(input);
    try 
    {
        ExecuteSql(reader.ReadToEnd());
    }
    finally 
    {
        reader.Close();
    }
}

例えばDbTestCaseを継承したテストケースをFooTestとし,FooTest/sample.sqlにSQLファイルを置けば次のようになる(注:sample.sqlはリソースとしてdllに埋め込むこと):

public class FooTest : DbTestCase
{
    [Test]
    public void Sample()
    {
        ExecuteSqlFile("sample.sql");
        ...
    }
    ...
}

これだけあればDBのテストもかなり楽に書けると思う.前の記事で紹介したSqlSelectConverterもあるし.

なお,以上の方法はテーブル間にリレーションなど制約があれば使えないかもしれない.幸いそこまで複雑なRDBのシステムを扱ったことはないので,そういう仕事が来ればそのときに考えることになるだろう.

08:20 PM in XP | 固定リンク

トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/419878

この記事へのトラックバック一覧です: DBのテスト:

コメント

コメントを書く