kamulog

xamarin.formsのネタなど

Xamarin関連でDeviceを使ったプラットフォームのUnitTestを行う

PCLプロジェクトのテストは、これまであんまりハマりどころは無かったのですが、プラットフォーム固有のコードをテストする場合に、いろいろとハマってきたので、そろそろまとめておきます。

プラットフォーム固有のテストが必要なパターン

  1. データベースの抽出処理や更新処理が正常に動作しているか確認したい時
  2. CustomRenderer等でプラットフォーム側でロジックが複雑になった時
  3. Plugin開発

DBなんかはsqlite-net-pclなんかを使えばPCLだけでほとんど完結するにはするんですが、テストとなると別で、実際にデータの入出力を確認したい時は、プラットフォーム側で確認するしかなかったりします。(私が知らないだけで本当は良いやり方があるのかもしれませんが…)

CustomRendererやEffectsはUIに関するものがほとんどだと思うので、テストのしようがない場合が多い(xamarin.formsに依存しまくっててどうにもならなかったり)ですが、一部のメソッドだけ確認したい場合なんかはやはりテストが必要かもしれません。

Pluginに関してはほとんどプラットフォーム側の処理なのでするしかないでしょうね。

何を利用するか

  1. Xamarin Studio組み込みのUnit Testプロジェクトを使う
  2. NUnit3 Xamarin Runnersを使う
  3. xunit.runner.devices を使う

1. Xamarin Studio組み込みのもの

これとか
f:id:kamusoft:20170325230318p:plain:w450
これ
f:id:kamusoft:20170325230542p:plain:w450
ですね。

基本的には、よくできているのでこれで大丈夫なのですが 1つ致命的な問題があって、非同期処理をすると処理が返ってこなくなります。

public async Task Test01(){
    var hoge = "abc";
    await heavy();  //ここで行方不明になります

    Assert.Equal(hoge,"abc");
}

というわけで非同期処理がある場合はこれは使えません。

2. NUnit3 Xamarin Runners

こちらはNugetで入れるタイプのRunnerで、NUnitでできることは全てできる感じで、もちろん非同期処理もちゃんとテストできます。

ですが…
f:id:kamusoft:20170325230751p:plain:h360

この画面見てもらえばわかると思いますが、Run Testsボタンしかなくて、これを押すと全てのテストが走ってしまいます。Run Allしかないんです。 というわけで使い物になりませんでした。

3. xunit.runner.devices

これは個別にテストも選べるし非同期処理も出来るしで、実質現状これしかないと思います。 というわけでxunit.runner.devicesを利用したテスト方法をこれ以降で説明していきます。

テストソリューションを作成

新しいソリューションを作成します。

f:id:kamusoft:20170325231038p:plain

Shared Libraryを選択します。 (多分プラットフォームのテストではSharedが便利かと思います。)

f:id:kamusoft:20170325231051p:plain

そのまま作成します。

f:id:kamusoft:20170325231102p:plain

補足

テスト対象のソリューションにテストプロジェクトを追加していく形でも本来は良いはずなんですが、どうもそうするとSharedプロジェクトでインテリセンスが全く効かない上にそもそもPlatform側からクラスが認識できないという問題が発生してしまいました。(Xamarin Studio Version 6.2 build 1821)

仕方なくテスト専用のソリューションを作成することで、回避できたのでこのような回りくどい方法になっています。

xunitとxunit.runner.devicesのインストール

各プラットフォーム(iOS/Android/UWP)にインストールします。

f:id:kamusoft:20170325231238p:plain

xunitのバージョンとxunit.runner.devicesのバージョンは合わせた方が良いです。 2017年3月2日現在ではxunitが2.2.0が最新でrunnerが2.1.0が最新となっており、2.2.0の方は.net standardに対応していて、runnerが非対応となってしまい、うまくビルドできませんでした。なのでxunitは2.1.0を選択してインストールします。

f:id:kamusoft:20170325231251p:plain

メインのcsを入れ替える

インストール直後はこのようになっています。

f:id:kamusoft:20170325231313p:plain

AndroidはMainActivity.cs.txtが追加されています。 もともとあったMainActivity.csを削除して、MainActivity.cs.txtの.txtを消すようにリネームします。

iOSの方はAppDelegate.csとAppDelegate.cs.txtを同様に入れ替えます。

準備OK!

これでビルドして実行できるようになりました。

f:id:kamusoft:20170325231333p:plain:w300f:id:kamusoft:20170325231339p:plain:w300

テスト対象のプロジェクトを追加

テストしたいプロジェクトをPCLと各プラットフォームの分を追加していきます。

f:id:kamusoft:20170325231357p:plain

補足

テスト対象のソリューションに作成したテストプロジェクトを追加しても良いです。 一度ソリューションとして作った後はSharedProjectがバグることもないみたいです。

参照設定

こんな感じでそれぞれのプラットフォームテストプロジェクトに追加したテスト対象プロジェクトを参照に加えます。例ではPlugin.ImageEditをテストしています。

Android

f:id:kamusoft:20170325231412p:plain

iOS

f:id:kamusoft:20170325231427p:plain

テストコードを書く!

Sharedプロジェクトに最初からあるcsの中身を消してテストクラス・メソッドを書いていきます。

namespace DeviceTest
{
    /// <summary>
    /// Fixture クラス
    /// 前処理、後処理など
    /// </summary>
    public class MainFixture : IDisposable
    {
        public ImageEdit ImageEdit { get; set; }
        //クラスで最初に一回だけ行う処理
        public MainFixture()
        {
#if __IOS__
            //iOSだけの処理

#endif
#if __ANDROID__
            //Androidだけの処理

#endif
            ImageEdit = new ImageEdit();

        }

        //クラスで最後に1回だけ行う処理
        public void Dispose()
        {

        }
    }

    /// <summary>
    /// テストクラス
    /// </summary>
    public class Main : IClassFixture<MainFixture>, IDisposable
    {
        MainFixture _fixture;

        //テストメソッドの前に毎回呼び出される
        public Main(MainFixture fixture)
        {
            _fixture = fixture;
        }

        [Fact]
        public void Test01()
        {
            Assert.NotNull(_fixture.ImageEdit);
        }

        [Fact]
        public async Task Test02()
        {
            await Task.Delay(250);
            Assert.True(true);
        }

        [Fact]
        public async Task Test03()
        {
            await Task.Delay(250);
            Assert.False(false);
        }

        //テストメソッドの後に毎回呼び出される
        public void Dispose()
        {

        }
    }
}

xunitではクラス単位での初期化メソッドや終了処理を定義するには別にFixture用クラスを作って、そこに定義するみたいです。

テスト実行

テストはそれぞれの端末またはシミュレータ上で実行します。

f:id:kamusoft:20170325231448p:plain:w300f:id:kamusoft:20170325231509p:plain:w300

無事動作しました。

プラットフォームの切り替えで同じsharedのテストコードが実行できます。

f:id:kamusoft:20170325231450p:plain f:id:kamusoft:20170325231451p:plain

もしプラットフォーム間で実装が大きく異なる場合はsharedではなく直接プラットフォームのプロジェクトにテストコードを書いていくのが良いと思います。

その他

テストメソッド名までが長すぎで表示しきれなくて見えない

なるべく大きい端末のシミュレータで行うか横向きにしましょう。 あとはnamespaceとかクラス名は極力短くすると見やすくなります。

Androidでパッケージ名が一致しません的なエラーメッセージが出る場合

テスト対象のパッケージ名にテストのパッケージ名を合わせると消えました。

複数のテストクラスが並列実行されて困る場合

デフォルトだと異なるクラスは並列実行されてしまって、DBの初期化、テストデータ挿入処理などを行っているとおかしなことになって困りました。

これは、Collection属性を付けて引数に同じ文字列を設定することで回避できました。

[Collection("Models")]
public class TestClass1
{
}

[Collection("Models")]
public class TestClass2
{
}

この場合TestClass1とTestClass2は同時実行されなくなります。

参考