はじめに
本記事は Xamarin Advent Calendar 2020の1日目の記事です。
XamarinでxUnit + Moqを使って単体テストを実装する際のコツを紹介します。
環境
- Visual Studio Community 2019 for Mac:8.8 (build 2913)
- xUnit:2.4.0
- Moq:4.14.7
- Xamarin.Essentials:1.5.3.2
実装
かんたんなC#のクラスと単体テストを実装します。
テスト対象クラスの実装
Xamarin.Essentials.AppInfo.VersionString
を返すだけの GetAppVersion()
メソッドを持った FooService
クラスを実装します。
usingXamarin.Essentials;namespaceFoo.Services{publicinterfaceIFooService{stringGetAppVersion();}publicclassFooService:IFooService{publicFooService(){}publicstringGetAppVersion(){returnAppInfo.VersionString;}}}
テストクラスの実装
GetAppVersion()
メソッドの戻り値が空でないことを確認するだけの単体テストを実装します。
usingFoo.Services;usingXunit;namespaceFoo.UnitTests.Services{publicclassFooServiceTests{[Fact]publicvoidOutputAppVersion(){varfooService=CreateDefaultFooService();varappVersion=fooService.GetAppVersion();Assert.NotEmpty(appVersion);}privateFooServiceCreateDefaultFooService(){returnnewFooService();}}}
単体テストの実行
上記の単体テストを実行すると、以下のエラーで失敗します。
Xamarin.Essentials.NotImplementedInReferenceAssemblyException : This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation.
どうやら Xamarin.*
のクラスを単体テストで呼び出せないようです。
(呼び出せる方法があれば教えていただけると嬉しいです)
解決策: 単体テストから呼び出せないクラスをモックに差し替える
単体テストから呼び出せないクラスはDIして、テスト時にモックへ差し替えるようにします。
AppInfoService
クラスを新規作成し、 FooService
クラスから Xamarin.Essentials
への依存を切り出します。
usingXamarin.Essentials;namespaceFoo.Services{publicinterfaceIAppInfoService{stringGetAppVersion();}publicclassAppInfoService:IAppInfoService{publicAppInfoService(){}publicstringGetAppVersion(){returnAppInfo.VersionString;}}}
FooService
クラスにコンストラクタ経由で AppInfoService
クラスをDIすれば、 Xamarin.Essentials
に依存しなくなります。
- using Xamarin.Essentials;
namespace Foo.Services
{
public interface IFooService
{
string GetAppVersion();
}
public class FooService : IFooService
{
+ private readonly IAppInfoService appInfoService;
- public FooService()
+ public FooService(IAppInfoService appInfoService)
{
+ this.appInfoService = appInfoService;
}
public string GetAppVersion()
{
- return AppInfo.VersionString;
+ return appInfoService.GetAppVersion();
}
}
}
単体テストではMoq(モックライブラリ)を使って IAppInfoService
のモックを生成し、 FooService
クラスにDIします。
モックで GetAppVersion()
メソッドの戻り値を 1.0.0
に固定し、アサートで 1.0.0
と一致するか確認します。
Moqはメソッドが何回呼ばれたかかんたんに確認できるので、1回のみ呼ばれたか( Times.Once()
)を確認します。
using Foo.Services;
+ using Moq;
using Xunit;
namespace Foo.UnitTests.Services
{
public class FooServiceTests
{
[Fact]
public void OutputAppVersion()
{
+ var mockIAppInfoService = CreateDefaultMockIAppInfoService();
- var fooService = CreateDefaultFooService();
+ var fooService = CreateDefaultFooService(mockIAppInfoService);
var appVersion = fooService.GetAppVersion();
- Assert.NotEmpty(appVersion);
+ Assert.Equal("1.0.0", appVersion);
+ Mock.Get(mockIAppInfoService).Verify(s => s.GetAppVersion(), Times.Once());
}
- private FooService CreateDefaultFooService()
+ private FooService CreateDefaultFooService(IAppInfoService appInfoService)
{
- return new FooService();
+ return new FooService(appInfoService);
}
+ private IAppInfoService CreateDefaultMockIAppInfoService()
+ {
+ var mock = Mock.Of<IAppInfoService>(s =>
+ s.GetAppVersion() == "1.0.0"
+ );
+
+ return mock;
+ }
}
}
これで単体テストを実行すると、無事に成功します
Xamarinでは Xamarin.*
に依存しないようにビジネスロジックのクラスを実装すると、テスタブルになります。
おわりに
以上、 Xamarin Advent Calendar 2020の1日目の記事でした。
明日は @ytabuchiさんの記事です。