Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9749

自作のWPFアプリを後から自動テスト・DI対応にしてみる。その1

$
0
0
概要 これはもともとあったMVVMなWPFアプリに自動テストとDependency-Injection(DI)を実装してみた記録です。変更点はWPFとはあまり関係ないので、C#であれば他のフレームワークにも参考になるかもしれません。 今回は自動テストの追加までです。 WPFアプリの中身はファイルリネーマーです。アプリの詳細はこちらで。コード行数850行ぐらい、クラス数40個ぐらいのサンプルアプリに毛の生えたぐらいのコードサイズです。 TestフレームワークはxUnit、DIはMicrosoft.Extensions.DependencyInjectionを使用しました。 自動テストの導入 自動テストプロジェクトの追加 WPFのアプリを含んだソリューションにxUnitのプロジェクトを追加します。 WPFアプリをテストするために、テストプロジェクトの.csprojファイルを編集します。 TargetFrameworkをnet5.0-windowsに、UseWPFを追加しました。 そしてテストしたいプロジェクトへの参照も追加します。 UnitTests.csproj <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> ... </ItemGroup> </Project> 👇 UnitTests.csproj <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWPF>true</UseWPF> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> ... </PackageReference> </ItemGroup> <ItemGroup> <ProjectReference Include="..\FileRenamerDiff\FileRenamerDiff.csproj" /> </ItemGroup> </Project> テストを書きやすくするために、nugetでFluentAssertionsパッケージも入れて、ついでに既存のパッケージも更新しておきます。 テストする対象にinternalメソッド・プロパティなどがある場合は、テストされる側のプロジェクト(WPFアプリ側)に、単体テストプロジェクトへの許可を書く必要があります。 AssemblyInfo.csを新しく作る方法もありますが、csprojに以下の5行を足すほうが簡単です。 FileRenamerDiff.csproj ... <ItemGroup> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"> <_Parameter1>UnitTests</_Parameter1> </AssemblyAttribute> </ItemGroup> ... VisualStudioを使っているなら、テストエクスプローラーを表示しておきます。 プロジェクト生成時にデフォルトで以下のようなテストが作成されます。 UnitTest1.cs public class UnitTest1 { [Fact] public void Test1() { } } ここにもろもろ足していきます。 この状態で試しにテスト実行すると、デフォルトのテストが成功します。 これで自動テストを書く準備ができました。 テストできるパターン まず、この段階でもテストできるパターンを書いてみます。 以下のようなクラスをテストします。 ValueHolder.cs public class ValueHolder<T> : NotificationObject { private T _Value; public T Value { get => _Value; set => RaisePropertyChangedIfSet(ref _Value, value); } public ValueHolder(T value) { this._Value = value; } } public static class ValueHolderFactory { public static ValueHolder<T> Create<T>(T value) => new(value); } ValueHolderクラスは値を内部に値を一つ保持して、変更されたらPropertyChangedイベントを発生させるだけのクラスです。いわば、ReactivePropertyからreactive要素を抜いたようなものです。 LivetのNotificationObjectを継承することで、INotifyPropertyChangedの通知が使えるようになっています。 このクラスは他のクラスに依存していないので、この段階でもテストすることができます。 UnitTest1.cs [Fact] public void Test_ValueHolder() { var queuePropertyChanged = new Queue<string?>(); var holder = ValueHolderFactory.Create(string.Empty); holder.PropertyChanged += (o, e) => queuePropertyChanged.Enqueue(e.PropertyName); holder.Value .Should().BeEmpty("初期値は空のはず"); queuePropertyChanged .Should().BeEmpty("まだ通知は来ていないはず"); const string newValue = "NEW_VALUE"; holder.Value = newValue; holder.Value .Should().Be(newValue, "新しい値に変わっているはず"); queuePropertyChanged.Dequeue() .Should().Be(nameof(ValueHolder<string>.Value), "Valueプロパティの変更通知があったはず"); } ValueHolderの変更通知を貯めておいて、変更前後に適切な通知が来るか確認しています。 テスト実行して、テストが成功したことを確認します。 FluentAssertionsを使うことでTest対象.Should().Be..のようにテストしたい対象に対して、メソッドチェーンにテストが書けます。 テストできないパターン 次に以下のようなクラスをテストします。 /// <summary> /// リネーム前後のファイル名を含むファイル情報モデル /// </summary> public class FileElementModel : NotificationObject { private readonly FileSystemInfo fsInfo; /// <summary> /// リネーム前 フルファイルパス /// </summary> public string InputFilePath => fsInfo.FullName; /// <summary> /// リネーム前 ファイル名 /// </summary> public string InputFileName => fsInfo.Name; private string outputFileName = "--.-"; /// <summary> /// リネーム後 ファイル名 /// </summary> public string OutputFileName { get => outputFileName; set => RaisePropertyChangedIfSet(ref outputFileName, value, new[] { nameof(IsReplaced) }); } /// <summary> /// リネーム後 ファイルパス /// </summary> public string OutputFilePath => Path.Combine(DirectoryPath, outputFileName); /// <summary> /// リネーム前後で変更があったか /// </summary> public bool IsReplaced => InputFileName != OutputFileName; /// <summary> /// ファイルの所属しているディレクトリ名 /// </summary> public string DirectoryPath => fsInfo.GetDirectoryPath() ?? string.Empty; public FileElementModel(string filePath) { this.fsInfo = new FileInfo(filePath); this.outputFileName = InputFileName; } /// <summary> /// 指定された置換パターンで、ファイル名を置換する(ストレージに保存はされない) /// </summary> internal void Replace(IReadOnlyList<ReplaceRegex> repRegexes) { string outFileName = InputFileName; foreach (var reg in repRegexes) { outFileName = reg.Replace(outFileName); } OutputFileName = outFileName; } /// <summary> /// リネームを実行(ストレージに保存される) /// </summary> internal void Rename() { fsInfo.Rename(OutputFilePath); fsInfo.Refresh(); //rename時にFileInfoが変更されるので、通知を上げておく foreach (var name in new[] { nameof(InputFileName), nameof(InputFilePath), nameof(IsReplaced) }) RaisePropertyChanged(name); } } FileElementModelクラスは1つのファイルに対してリネームプレビュー・リネーム実行をするクラスです。 そして、これに対するテストを書いてみます。 [Fact] public void Test_FileElement() { var fileElem = new FileElementModel(@"D:\FileRenamerDiff_Test\coopy -copy.txt"); fileElem.OutputFileName .Should().Be("coopy -copy.txt", "まだ元のファイル名のまま"); fileElem.IsReplaced .Should().BeFalse("まだリネーム変更されていないはず"); //ファイル名の一部をXXXに変更する置換パターンを作成 var regex = new Regex(" -copy", RegexOptions.Compiled); var rpRegex = new ReplaceRegex(regex, "XXX"); //リネームプレビュー実行 fileElem.Replace(new[] { rpRegex }); fileElem.OutputFileName .Should().Be("coopyXXX.txt", "リネーム変更後のファイル名になったはず"); fileElem.IsReplaced .Should().BeTrue("リネーム変更されたはず"); //リネーム保存実行 fileElem.Rename(); fileElem.InputFileName .Should().Be("coopyXXX.txt", "リネーム保存後のファイル名になったはず"); } 事前に変更するファイルをファイルエクスプローラーで用意しておきます。 このまま、テスト実行してみましょう。 なんと無事に成功します!よかったよかった。。。 成功したテストを眺めると安心感が得られるので、そのままもう一度実行してみましょう。 するとどういうことでしょうか、失敗しました!! 「何もしてないのに壊れた!!💢」 もちろん、これは当たり前で、実際のファイル名の変更をするテストは、実際のファイル名が変更されてしまいますし、 ファイル名が変われば、実際のファイル名の変更をするテストの結果は異なってしまいます。 つまり、このパターンの問題点は以下の点です。 テストするために実際のファイルにアクセスしている それゆえ、テスト結果が環境に依存する つまりCI/CDできない テストによって環境が変化してしまう それゆえ、テストをやるたびに結果が変わる というわけで、この問題をDIを使って解決していきます。 参考 https://xunit.net/ https://qiita.com/takutoy/items/84fa6498f0726418825d 環境 VisualStudio 2019 C# 9 .NET 5 xUnit 2.4.1

Viewing all articles
Browse latest Browse all 9749

Trending Articles