読者です 読者をやめる 読者になる 読者になる

きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

SQL Server Data Toolsのユニットテスト実行前にLocalDBのインスタンスを破棄・再作成する

.NET Visual Studio SSDT SQL Server LocalDB

SQL Server Data Tools (SSDT)のユニットテスト機能ネタです。

テスト実行前の自動デプロイは便利だが失敗するケースがある

以前の記事(これとかこれ)でも触れた、SSDTの ユニットテスト実行前にDBインスタンスに自動でデプロイを行う機能 は非常に便利です。DBプロジェクト側で変更を普通に保存した後、テストランナーでテストを走らせるだけで環境を最新にして実行してくれます。

便利なのですが、この自動デプロイが必ず失敗するケースがあります。

例えばデータが入っているテーブルの列を削除したり列の型を変更するなどデータ消失が発生する可能性があるような変更がある場合です*1

もちろん一旦手動で削除してから実行することはできますが、手間ですしCIでの自動テストの障害にもなります。

DBプロジェクトの発行機能では発行プロファイルの設定でデータベースを毎回再作成するオプションがありますが、どうやらテスト実行時の発行には利用できないようです。

再実行時に単純に削除して再作成すればよいのであれば*2、SQL Server Express LocalDB(以下LocalDB)と組み合わせて以下のような方法を思いつきました。

 SQL LocalDB Wrapper を使ってデプロイ前にLocalDBのインスタンスを削除・再作成する

LocalDBの場合、簡単にインスタンスを削除するすることができます。

またSQL Server LocalDBのインスタンスを削除してもユーザーデータベースのファイルが残る でも紹介した SQL LocalDB Wrapperを使うと、LocalDBのインスタンスをSSDTの自動デプロイで作成されたデータベースごときれいに削除できます。SQL LocalDB Wrapperにはインスタンスを再作成する機能もあります。

これらを利用すると、テストの初期化時、データベースのデプロイが始まる前にLocalDBのインスタンスを破棄・再作成が簡単に行えます。

組み込み例

組み込み方は非常に簡単です。テストプロジェクトにNuGetでSQL LocalDB Wrapperを取り込んだ後、例えば、適当に以下のようなメソッドを作ってやります。

/// <summary>
///  LocalDBのインスタンスの再作成
/// </summary>
/// <remarks>
/// SQL LocalDB Wrapper経由でネイティブAPIを通して操作する
/// https://www.nuget.org/packages/System.Data.SqlLocalDb/
/// </remarks>
internal void RecreateLocalDbInstance(string instanceName, string version)
{
  // インスタンスが存在したらインスタンスを停止してからユーザーDBも同時に削除してから作成
  if (SqlLocalDbApi.GetInstanceNames().Contains(instanceName))
  {
    SqlLocalDbApi.StopInstance(instanceName, StopInstanceOptions.KillProcess, new TimeSpan(0, 0, 30));
    SqlLocalDbApi.DeleteInstance(instanceName, true);
  }
  SqlLocalDbApi.CreateInstance(instanceName, version);
}

余談ですが、SqlLocalDbApi.CreateInstance(String, String)の第二引数を渡さない(オーバーロードのSqlLocalDbApi.CreateInstance(String)を呼ぶ)と、作成されるインスタンスのバージョンは環境にインストールされている最新のLocalDBにものになるようです(ざっとソースを見た感じ)。なので指定しなくても問題はないのですが、たとえば複数人による開発で実行環境にばらつきがあるような場合は、ここはあえて指定するようにしておいたほうが無難です。この詳細はまた別途改めて書きたいと思っています。*3

あとは先ほど作ったRecreateLocalDbInstanc()をSqlDatabaseSetup.csのSqlDatabaseSetup.InitializeAssemblyメソッドの頭のほうで呼んでやればOKです。

  [TestClass()]
  public class SqlDatabaseSetup
  {
    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
        RecreateLocalDbInstance("SomeDbInstance","13.0");

     // ...既存の処理はそのまま
    }
  }

まとめ

SQL LocalDB Wrapperを使うとC#のコード中からLocalDBを簡単に操作出来て便利ですね。 まだぼんやりとしか考えてないのですが、アプリケーションのユニットテストでDBに接続する必要があるようなテストでもこれを利用していい感じにできるんじゃないかとか思ったりしてます。

*1:テスト自体にトランザクションを利用してデータが残らないような状態にキープしておくのであればこの問題は発生しません。しかし、例えばバッチ更新のストアドのテストで失敗したときにテーブルの状態を確認したい場合や、値が原則変わらないマスタ類をテスト開始時に投入ておきたいなどの要件と相容れません

*2:単体テストなのでそうあるべきとは思う

*3:何時になるかわからないので簡単にメモしておきます。SQL Server 2014以前のデータベースのコンポーネントが入っている環境で、バージョン13.0(SQL Server 2016相当)のLocalDBのインスタンスへのテスト実行前自動デプロイが失敗することがありました。SQL Server 2016のSSMSをインストールしたり、LocalDBのインスタンスのバージョンを12.0で作成するとうまくいくことから、13.0のLocalDBに対して12.0までしか対応していないコンポーネントを使って接続しようとして失敗してるように見えました。もう少し状況を絞りたいので現状はこれ以上は追加調査できていません。