リフレクション 以外に方法ありますかね。ご存知の方お手すきにご教示頂ければ。
追記: (Expression でもできるのでは、と教えていただく。知識が追い付いていないので記事には未反映)
完成形の一例(テストコード)
using System.Reflection;
using UnitTestStaticMember.Model;
namespace NUnit.Reflection
{
[TestFixture]
public class UserManagerTest
{
[Test]
public void AuthorizedUserList()
{
// ここからリフレクション
// private static メンバ変数に割り当てる値を用意
var authzCode = new List<AuthorityCode>() { AuthorityCode.S, AuthorityCode.A };
// 値割り当てする private static メンバ変数を FieldInfo で参照する
FieldInfo authorizedCode = typeof(ServiceManager).GetField("authorizedCode", BindingFlags.NonPublic | BindingFlags.Static);
// メンバ変数、メンバ変数を保持しているクラス、割り当てたい値、それぞれを関連付ける
authorizedCode.SetValue(new ServiceManager(), authzCode);
// ここまでリフレクション
var user1 = Substitute.For<IUser>();
user1.AuthCode.Returns(AuthorityCode.A);
var userList = new List<IUser>() { user1 };
var userManger = new UserManager(userList);
Assert.AreEqual(1, userManger.AuthorizedUserList().Count);
}
}
}
例題プロジェクト
ユーザー(名前、権限コード を所有)が何人か存在し
サービスが認可した権限コードを所有したユーザーを表示する
というコンソールアプリを示す。
github にリポジトリ配置済み。ご利用はご自由にどうぞ。
プロジェクト構成
アプリ本体
以下記載通りで特記なし。組み込まれているクラスを追っていく。
ユーザー(名前、権限コード を所有)が何人か存在し
サービスが認可した権限コードを所有したユーザーを表示する
using UnitTestStaticMember.Model;
namespace UnitTestStaticMember
{
class Program
{
static void Main(string[] args)
{
// サービス情報を初期化する
// Init 内で "認可された 権限コード(AuthorityCode) のリスト" を初期化する
ServiceManager.Init();
// User(Name, AuthorityCode) のリストを作成する(userManager に渡すため)
var users = new List<IUser>()
{
new User("foo", AuthorityCode.A), new User("bar", AuthorityCode.B)
};
// サービスにて認可された 権限コード(AuthorityCode) を所有したユーザーだけを表示する
// プロジェクト実装上 User:"foo" だけが表示される
var userManager = new UserManager(users);
var authzUserList = userManager.AuthorizedUserList();
foreach(var authzUList in authzUserList)
{
System.Console.WriteLine(authzUList.Name);
}
}
}
}
サービスのデータ管理クラス
Init()
により初期化したデータを static メンバ変数
に突っ込んでいく。
authorizedCode
は後出ユーザー管理クラス UserManager
内の機能で利用する。
namespace UnitTestStaticMember.Model
{
public class ServiceManager
{
/// <summary>
/// アプリ立ち上げ時に実行するメソッド
/// 初期化処理を突っ込む
/// </summary>
public static void Init()
{
InitServiceAuthorizedCode();
}
/// <summary>
/// 初期化した static 変数を取得するメソッド
/// 徒に static 変数を public へしないための対策
/// </summary>
/// <returns></returns>
public static List<AuthorityCode> GetServiceAuthorizedCode()
{
return authorizedCode;
}
private static void InitServiceAuthorizedCode()
{
authorizedCode.Add(AuthorityCode.S);
authorizedCode.Add(AuthorityCode.A);
}
private static List<AuthorityCode> authorizedCode = new List<AuthorityCode>();
}
}
定数管理クラス
権限コードを定数化したもの。サービスのデータ管理クラスでは
S
, A
を認可した権限コードとして扱っている。
namespace UnitTestStaticMember.Model
{
public enum AuthorityCode
{
S, A, B, C
}
}
ユーザーモデル
一人一人のユーザーを表現するクラス。名前と権限コードを持っている。
namespace UnitTestStaticMember.Model
{
public interface IUser
{
string Name { get; }
AuthorityCode AuthCode { get; }
}
public class User : IUser
{
public User(string name, AuthorityCode authorityCode)
{
this.name = name;
this.authorityCode = authorityCode;
}
public string Name
{
get { return this.name; }
}
public AuthorityCode AuthCode
{
get { return authorityCode; }
}
private string name;
private AuthorityCode authorityCode;
}
}
ユーザー管理クラス
ユーザーに関わるデータを管理するクラス。
AuthorizedUserList()
より
サービスで認可された権限コードを所有したユーザーを取得する
機能を提供する。
namespace UnitTestStaticMember.Model
{
public class UserManager
{
public UserManager(List<IUser> userList)
{
this.userList = userList;
}
/// <summary>
/// get users they have service's authorized code; S or A.
/// return type is list.
/// </summary>
public List<IUser> AuthorizedUserList()
{
var authzCodes = ServiceManager.GetServiceAuthorizedCode();
return userList.Select(user => user)
.Where(user => authzCodes.Contains(user.AuthCode))
.ToList();
}
private List<IUser> userList;
}
}
本題
単体テストを作る
Model/UserManager.cs
の AuthorizedUserList()
を NUnit
で単体テストを作る。
以下のような単体テストがひとまず考えられる。
namespace NUnit.Reflection
{
[TestFixture]
public class UserManagerTest
{
[Test]
public void AuthorizedUserList()
{
var user1 = Substitute.For<IUser>();
user1.AuthCode.Returns(AuthorityCode.A);
var userList = new List<IUser>() { user1 };
var userManger = new UserManager(userList);
Assert.AreEqual(1, userManger.AuthorizedUserList().Count);
}
}
}
課題
テストを作ったは良いものの、検証してみると
Assert.AreEqual(1, userManger.AuthorizedUserList().Count);
より userManger.AuthorizedUserList().Count
が 0
になる。
userManger.AuthorizedUserList()
> ServiceManager.GetServiceAuthorizedCode()
が空リストを返しているからだ。
もう少し突っ込むとテスト実施前に ServiceManager.Init()
していなかったからだ。
(横道にずれて... 単体テストという観点で考えると、何を考えるべきか)
少し立ち止まって単体テストという観点では「検証する機能以外の機能」はスタブにしておきたい。例えばの話
ServiceManager.Init()
することで、どうしても ステージングサーバーに繋がってしまう(そういうアレな)
仕組みが出来上がっているならば ServiceManager.Init()
の利用は避けたいものだ。
どうにかしてスタブできるものはスタブしよう。
課題の探索
-
Model/UserManager.cs
>ServiceManager.GetServiceAuthorizedCode()
がある - このメソッドをスタブしたい
-
GetServiceAuthorizedCode()
はstatic メソッド
だからスタブ難しそう(できるかどうか) -
GetServiceAuthorizedCode()
メソッド内でreturn
しているstatic メンバ変数
はスタブできないか-
private
だ。どうしようか。
-
-
方針案
リフレクションを使って、private static メンバ変数のデータ
はテスト時、動的に書き換えられる様子。
これをやろう。
テストコード案
using System.Reflection;
using UnitTestStaticMember.Model;
namespace NUnit.Reflection
{
[TestFixture]
public class UserManagerTest
{
[Test]
public void AuthorizedUserListNotEmpty()
{
// ここからリフレクション
// ServiceManager > authorizedCode(private static) に値割り当てる
// 結果的に ServiceManager > GetServiceAuthorizedCode() の返却値がスタブされている
var authzCode = new List<AuthorityCode>() { AuthorityCode.S, AuthorityCode.A };
FieldInfo authorizedCode = typeof(ServiceManager).GetField("authorizedCode", BindingFlags.NonPublic | BindingFlags.Static);
authorizedCode.SetValue(new ServiceManager(), authzCode);
// ここまでリフレクション
var user1 = Substitute.For<IUser>();
user1.AuthCode.Returns(AuthorityCode.A);
var userList = new List<IUser>() { user1 };
var userManger = new UserManager(userList);
Assert.AreEqual(1, userManger.AuthorizedUserList().Count);
}
}
}