数千万円以上する他社製品を操作するアプリの開発を行う場合、他社製品に接続できるのは現地だけで、開発作業を行うローカルPCからは他社製品に接続できないなど、作業できる環境が限られ、デバッグ作業が思うように進まないということがよく起きます。
その場合、他社製品のI/F仕様と一致するStubを作成し、開発作業を行うローカルPCでは、Stubを使いデバッグや試験を進めることで、開発作業のスピードと精度を上げ、現地で発生する問題は、他社製品側の不具合のみとすることができます。
他社製品とセットで提供される、他社製品を操作する為のI/Fライブラリ(DLL)から、Publicのメソッドだけをピックアップし、I/Fの側(がわ)だけを実装したStubを作れば、他社製品に接続できないローカルでも、デバッグや試験を進めることが出来ます。
数千万円以上する他社製品に接続する為のI/Fライブラリは、DLLで提供されることが多いので、今回、他社から提供されたDLLのStubを、どうやって作るのか纏めました。
モック(Moq)じゃないです、スタブ(Stub)です。
モックとスタブの区別がつかず、スタブを作る話をしていて「それモックだろ!?」と言って来る人、多過ぎ。
ソースコードはGitHubで公開しています。
.Net Coreをベースに作りましたが、.Net Framework でも同じ実装になります。
他社製品 I/Fライブラリ(DLL)
他社製品 I/Fライブラリ(DLL)として、ThirdPartyProducts.dll を作成しました。
仕様
- IsStartedプロパティ:操作対象が起動済みかなどのステータスを返す。
- Startupメソッド:操作対象を起動する。
- Transaction1メソッド:最初の処理を実行する。
- Transaction2メソッド:次の処理を実行する。
- Shutdownメソッド:操作対象を停止する。
Visual Studio プロジェクト
.Net Core 5.0 のクラスライブラリ。
ソースコード構成
ビルドすると ThirdPartyProducts.dll が作成される。
ソースコード解説
ThirdPartyProducts.sln
他社製品 I/Fライブラリ(DLL)のソリューション。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.32802.440 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThirdPartyProducts", "ThirdPartyProducts\ThirdPartyProducts.csproj", "{83B693BE-8E02-41F1-92F6-2FEFFD316186}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {83B693BE-8E02-41F1-92F6-2FEFFD316186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {83B693BE-8E02-41F1-92F6-2FEFFD316186}.Debug|Any CPU.Build.0 = Debug|Any CPU {83B693BE-8E02-41F1-92F6-2FEFFD316186}.Release|Any CPU.ActiveCfg = Release|Any CPU {83B693BE-8E02-41F1-92F6-2FEFFD316186}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {540CB6BB-C9AB-4E1D-BDC9-3E12B0EE090A} EndGlobalSection EndGlobal |
ThirdPartyProducts/ThirdPartyProducts.csproj
ThirdPartyProducts.dll のプロジェクト。
1 2 3 4 5 6 7 8 9 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> </Project> |
ThirdPartyProducts/enums.cs
ThirdPartyProducts.dll が返す、StatusとReturnCodeの、enumを纏めたソースファイル。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ThirdPartyProducts { public enum Status : uint { /// <summary> /// 未起動 /// </summary> NotStarted = 0x00000000, /// <summary> /// 起動済み /// </summary> Started = 0x00000001, /// <summary> /// 処理1は終了している /// </summary> Transaction1_End = 0x00000002, /// <summary> /// 処理2は終了している /// </summary> Transaction2_End = 0x00000003, } public enum ReturnCode : uint { /// <summary> /// 正常終了 /// </summary> Success = 0x00000000, /// <summary> /// 異常終了 /// </summary> Error = 0x00000010, } } |
ThirdPartyProducts/ThirdPartyProductOps.cs
他社製品 I/Fライブラリ(DLL)のビジネスロジック。
他社製品 I/Fライブラリのサンプルなので、エラーチェックや、仕様に合わせた return値を返しているくらいで、内容はとくにないスケルトンになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
using System; using System.Reflection; using System.Threading; namespace ThirdPartyProducts { public static class ThirdPartyProductOps { private static Status _IsStarted = Status.NotStarted; public static Status IsStarted { get { Thread.Sleep(1000); return _IsStarted; } } /// <summary> /// 製品を起動 /// </summary> public static ReturnCode Startup() { Thread.Sleep(1000); if (_IsStarted == Status.Started) { //既に起動してる return ReturnCode.Error; } else { _IsStarted = Status.Started; return ReturnCode.Success; } } /// <summary> /// 処理1 /// </summary> public static ReturnCode Transaction1() { Thread.Sleep(1000); if (_IsStarted < Status.Started) { //起動していない return ReturnCode.Error; } _IsStarted = Status.Started; return ReturnCode.Success; } /// <summary> /// 処理2 /// </summary> public static ReturnCode Transaction2() { Thread.Sleep(1000); if (_IsStarted < Status.Transaction2_End) { //処理1がまだ終わっていない return ReturnCode.Error; } return ReturnCode.Success; } /// <summary> /// 製品を停止 /// </summary> public static ReturnCode Shutdown() { Thread.Sleep(1000); if (_IsStarted == Status.Started) { _IsStarted = Status.NotStarted; return ReturnCode.Success; } else { //既に停止してる return ReturnCode.Error; } } } } |
自社製品
他社製品 I/Fライブラリ(DLL)を通して、他社製品を画面から操作するアプリとして、Windows Formアプリを実装しました。
数千万円以上する他社製品を操作する為の業務アプリを開発するような場合、Windowsフォームアプリで実装していることが多い気がします。
下記動画は、他社製品 I/Fライブラリ(DLL)を仕様通り操作、実行していますが、これを現地じゃないと実行できないというのが問題になります。
仕様
- Windows Formアプリの画面から、ThirdPartyProducts.dll の各I/Fを実行する。
Visual Studio プロジェクト
今回使った Visual Studio 2019 プロジェクト テンプレートは、.Net Core 5.0 の Windowsフォームアプリ。
ソースコード構成
ソースコード解説
InHouseProduct.sln
他社製品 I/Fライブラリ(DLL)を、自社製品 Windows Formアプリから実行するソリューション。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.32802.440 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InHouseProduct", "InHouseProduct\InHouseProduct.csproj", "{B7D2EE19-12D8-4A28-944E-3D1D2AA923C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B7D2EE19-12D8-4A28-944E-3D1D2AA923C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B7D2EE19-12D8-4A28-944E-3D1D2AA923C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7D2EE19-12D8-4A28-944E-3D1D2AA923C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7D2EE19-12D8-4A28-944E-3D1D2AA923C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5674779C-E7A5-428C-9317-B0BAF65E1E89} EndGlobalSection EndGlobal |
InHouseProduct/InHouseProduct.csproj
自社製品 Windows Formアプリのプロジェクト。
ThirdPartyProducts.dll を参照先に追加し使えるようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net5.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup> <ItemGroup> <Reference Include="ThirdPartyProducts"> <HintPath>..\ClassLibrary_ThirdPartyProductsCapsule\ThirdPartyProductsLib\ThirdPartyProducts.dll</HintPath> </Reference> </ItemGroup> </Project> |
InHouseProduct/ThirdPartyProductsLib/ThirdPartyProducts.dll
ThirdPartyProducts.csproj のビルド結果DLLを配置し、InHouseProduct.csprojで参照設定している。
InHouseProduct/Form1.resx Form1.Designer.cs
他社製品 I/Fライブラリ(DLL)を操作する Windows Formアプリ画面の定義。
詳細は割愛します。
InHouseProduct/Form1.cs
他社製品 I/Fライブラリ(DLL)を操作する Windows Formアプリ画面のイベントハンドラ。
他社製品 I/Fライブラリの仕様通り、I/Fを順番に実行しています。
Debug.WriteLine()でイベントが実行されたことと実行結果が、分かるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using ThirdPartyProducts; namespace WinFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnIsStarted_Click(object sender, EventArgs e) { Debug.WriteLine("Execute IsStarted... "); var result = ThirdPartyProductOps.IsStarted.ToString(); Debug.WriteLine("Result: " + result); } private void btnStartup_Click(object sender, EventArgs e) { Debug.WriteLine("Execute Started... "); var result = ThirdPartyProductOps.Startup().ToString(); Debug.WriteLine("Result: " + result); } private void btnTransaction1_Click(object sender, EventArgs e) { Debug.WriteLine("Execute Transaction1... "); var result = ThirdPartyProductOps.Transaction1().ToString(); Debug.WriteLine("Result: " + result); } private void btnTransaction2_Click(object sender, EventArgs e) { Debug.WriteLine("Execute Transaction2... "); var result = ThirdPartyProductOps.Transaction2().ToString(); Debug.WriteLine("Result: " + result); } private void btnShutdown_Click(object sender, EventArgs e) { Debug.WriteLine("Execute Shutdown... "); var result = ThirdPartyProductOps.Shutdown().ToString(); Debug.WriteLine("Result: " + result); } private void btnAllExecute_Click(object sender, EventArgs e) { Debug.WriteLine("Execute IsStarted... "); var result = ThirdPartyProductOps.IsStarted.ToString(); Debug.WriteLine("Result: " + result); Debug.WriteLine("Execute Started... "); result = ThirdPartyProductOps.Startup().ToString(); Debug.WriteLine("Result: " + result); Debug.WriteLine("Execute Transaction1... "); result = ThirdPartyProductOps.Transaction1().ToString(); Debug.WriteLine("Result: " + result); Debug.WriteLine("Execute Transaction2... "); result = ThirdPartyProductOps.Transaction2().ToString(); Debug.WriteLine("Result: " + result); Debug.WriteLine("Execute Shutdown... "); result = ThirdPartyProductOps.Shutdown().ToString(); Debug.WriteLine("Result: " + result); } } } |
InHouseProduct/Program.cs
Visual Studioプロジェクトを新規作成してから変更はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace WinFormsApp1 { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } } |
自社製品(Stub)
他社製品 I/Fライブラリ(DLL)のStubを作成し、自社製品の InHouseProductから、他社製品 I/Fライブラリ(DLL)ではなく、Stubを操作できるソリューションを新たに作成。
InHouseProduct.csproj は別のファイルですが、それ以外のファイルはリンクとして追加します。
InHouseProduct.csproj は、他社製品 I/Fライブラリ(DLL)ではなく、他社製品 I/Fライブラリ(DLL)のStubを参照させます。
他社製品 I/Fライブラリ(DLL)の仕様通りの順番で、処理が実行されていることをローカルでデバッグ、試験できます。
Visual Studio プロジェクト
InHouseProductは、.Net Core 5.0 の Windowsフォームアプリ。
ThirdPartyProducts_Strubは、.Net Core 5.0 のクラスライブラリ。
ソースコード構成
ソースコード解説
InHouseProduct_onStub.sln
他社製品 I/Fライブラリ(DLL)の Stubを、自社製品 Windows Formアプリから実行するソリューション。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.32802.440 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InHouseProduct", "InHouseProduct\InHouseProduct.csproj", "{10C59127-32BB-4AAE-BA4C-91B6138CA1B4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThirdPartyProducts", "ThirdPartyProducts_Strub\ThirdPartyProducts.csproj", "{AA135B0D-BAC1-4590-96E8-B9C65D49F65D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {10C59127-32BB-4AAE-BA4C-91B6138CA1B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10C59127-32BB-4AAE-BA4C-91B6138CA1B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {10C59127-32BB-4AAE-BA4C-91B6138CA1B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {10C59127-32BB-4AAE-BA4C-91B6138CA1B4}.Release|Any CPU.Build.0 = Release|Any CPU {AA135B0D-BAC1-4590-96E8-B9C65D49F65D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA135B0D-BAC1-4590-96E8-B9C65D49F65D}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA135B0D-BAC1-4590-96E8-B9C65D49F65D}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA135B0D-BAC1-4590-96E8-B9C65D49F65D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5674779C-E7A5-428C-9317-B0BAF65E1E89} EndGlobalSection EndGlobal |
ThirdPartyProducts_Strub/ThirdPartyProducts.csproj
他社製品 I/Fライブラリ(ThirdPartyProducts.dll)のStubプロジェクト。
1 2 3 4 5 6 7 8 9 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> </Project> |
ThirdPartyProducts_Strub/enums.cs
enums.csは、他社製品 I/Fライブラリ(DLL)の外部公開されている仕様書を元に作るか、ソースコードが提供されていればソースファイルからコピーするか、dotPeekで他社製品 I/Fライブラリ(DLL)を逆コンパイルするか、いくつかの方法で作ることができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ThirdPartyProducts { public enum Status : uint { /// <summary> /// 未起動 /// </summary> NotStarted = 0x00000000, /// <summary> /// 起動済み /// </summary> Started = 0x00000001, /// <summary> /// 処理1は終了している /// </summary> Transaction1_End = 0x00000002, /// <summary> /// 処理2は終了している /// </summary> Transaction2_End = 0x00000003, } public enum ReturnCode : uint { /// <summary> /// 正常終了 /// </summary> Success = 0x00000000, /// <summary> /// 異常終了 /// </summary> Error = 0x00000010, } } |
ThirdPartyProducts_Strub/ThirdPartyProductOps.cs
他社製品 I/Fライブラリ(ThirdPartyProducts.dll)のStubプロジェクトのロジック。
ThirdPartyProducts.dllの I/Fは完全に合わせていますが、内部ロジックは何もないです。
Debug.WriteLine()をそれぞれのプロパティ/メソッドに追加し、プロパティ/メソッドが呼ばれたログとして使えるようにしています。
プロパティ/メソッドが呼ばれたログは、他社製品の仕様に沿って実行しているか、試験結果のエビデンスとして使えます。
Debug.WriteLine()だけではなく、ログファイルへ出力するのもお勧め。
作り方は enums.cs と同じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using System; using System.Diagnostics; using System.Reflection; using System.Threading; namespace ThirdPartyProducts { public static class ThirdPartyProductOps { private static Status _IsStarted = Status.NotStarted; public static Status IsStarted { get { Debug.WriteLine("Call : " + typeof(ThirdPartyProductOps).Name + "." + MethodBase.GetCurrentMethod().Name); Thread.Sleep(1000); return _IsStarted; } } public static ReturnCode Startup() { Debug.WriteLine("Call : " + typeof(ThirdPartyProductOps).Name + "." + MethodBase.GetCurrentMethod().Name); Thread.Sleep(1000); return ReturnCode.Success; } public static ReturnCode Shutdown() { Debug.WriteLine("Call : " + typeof(ThirdPartyProductOps).Name + "." + MethodBase.GetCurrentMethod().Name); Thread.Sleep(1000); return ReturnCode.Success; } public static ReturnCode Transaction1() { Debug.WriteLine("Call : " + typeof(ThirdPartyProductOps).Name + "." + MethodBase.GetCurrentMethod().Name); Thread.Sleep(1000); return ReturnCode.Success; } public static ReturnCode Transaction2() { Debug.WriteLine("Call : " + typeof(ThirdPartyProductOps).Name + "." + MethodBase.GetCurrentMethod().Name); Thread.Sleep(1000); return ReturnCode.Success; } } } |
InHouseProduct/InHouseProduct.csproj
Stubプロジェクト(ThirdPartyProducts.csproj)を対象に、自社製品 Windows Formアプリを実行できるようにした、別の InHouseProduct.csprojプロジェクト。
InHouseProduct.csprojを分けて、他社製品 I/Fライブラリ(DLL)の参照先を、ThirdPartyProducts.dll ではなく、ThirdPartyProducts_Strubプロジェクトに変えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net5.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup> <ItemGroup> <Compile Include="..\..\InHouseProduct\InHouseProduct\Form1.cs" Link="Form1.cs" /> <Compile Include="..\..\InHouseProduct\InHouseProduct\Form1.Designer.cs" Link="Form1.Designer.cs" /> <Compile Include="..\..\InHouseProduct\InHouseProduct\Program.cs" Link="Program.cs" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="..\..\InHouseProduct\InHouseProduct\Form1.resx" Link="Form1.resx" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\ThirdPartyProducts_Strub\ThirdPartyProducts.csproj" /> </ItemGroup> </Project> |
InHouseProduct.csprojファイル以外のオブジェクトは全て、自社製品側のオブジェクトに対するリンクとして追加している。
最新版ではより進化しています => スタブ(Stub)の作り方 v2
コメント