Status Code 303 - See Other

Java, C, C++, C#, Objective-C, Swift, bash, perl, ruby, PHP, Python, Scala, Groovy, Go, DevOps, Raspberry Pi など。情報の誤りや指摘・意見などは自由にどうぞ。

c#で値の未設定/デフォルト値を見分ける方法

概要

何も値が設定されていない時は、明示的に null を設定した時と挙動を分けたいことがある。
しかし、c# には javascript の undefine リテラルに相当するリテラルが存在せず、オブジェクトに何も設定せずに参照すると default(T) になるため何も値を設定していないのか、default(T) を設定されたのか分からず困る。

例えば以下のような、作成・更新データ形式をクラス構造で表現している時。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            // member.Age が デフォルト値(0)なら、データを 0 にする? それとも更新しない?
            // member.Name が デフォルト値(null) なら、データを null にする? それとも更新しない?
        }
    }

    public class Member {
        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

作成時には、データある/なしの場合は、値を指定する/ null で可能なのだが、
更新時には、これ以外に 「更新しない」という挙動が基本的に必要だ。そうしなければユーザの意図せず値がデフォルト値に更新されてしまうからだ。
これをどのように実現するかをメモっておく。

解決方法考察

  • 更新しないための「定数」を用意
  • Update引数をMember →Dictionary
  • 値の未設定/デフォルト値設定を分別できるオブジェクトにする
更新しないための「定数」を用意

「更新しない」という定義がなけりゃそう言った意味の定数を定義すれば良い。という考え方。
この方法はわかりやすいが危なっかしいコードになりやすい。コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (member.Age == Member.AgeNotUpdate) // Age を更新しない
            if (member.Name == Member.NameNotUpdate) // Name を更新しない
        }
    }

    public class Member {
        public static readonly int AgeNotUpdate = 13579;
        public static readonly string NameNotUpdate = "########";

        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

このコードを使う時は、下記のように使う。

        var data = new Member();
        data.Id = "123456";
        data.Age = 23;      // data.Age = 13579; の場合は変更しようとしても更新されない
        data.Name = null; //null にしたい場合
        // data.Name = Member.NameNotUpdate; //これで更新されない。ただし分かり辛い...
        new MemberManager().Update(data);

このコードの問題は、その「定数値」の更新処理ができなくなること。
もし、その値を偶然指定されてしまった場合は分かり辛いバグになること。
この方式自体が使う側にとっては直感的に分かり辛いこと。

Update引数をMember →Dictionary

そもそもオブジェクトがデフォルト値と明示的に設定されたかを区別できないことが問題なのだから、データ定義を変えればいい。という考え方。
この方法は「定数」による解決で発生する問題は解決できる。

しかし、Dictionary はデータ構造を共通にしないといけないため、型チェックができずメンテナンス性が悪くなりやすい。
コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Dictionary<Member.Key, object> member)
        {
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (!member.ContainsKey(Member.AgeKey)) // Age を更新しない
            if (!member.ContainsKey(Member.NameKey)) // Name を更新しない
        }
    }
    public class Member {
        // ハッシュキー
        public static readonly Key IdKey = new Key("Id");
        public static readonly Key AgeKey = new Key("Age");
        public static readonly Key NameKey = new Key("Name");
        public class Key {
            public string Value { get; }
            private Key(string value) { Value = value; }
        }

        public string Id { get; set; }
        public int Age { get; set; }
        public string Name { get; set; }
    }

このコードを使う時は、下記のように使う。

        var data = new Dictionary<Member.Key, object>()
        {
            { Member.IdKey, "123456" }
            { Member.AgeKey, 23 }
            { Member.NameKey, "AAA" } // 更新しない時はこの記載を外す。
            //{ Member.NameKey, null } // null にしたければ null を代入する。
        }
        new MemberManager().Update(data);

「更新しない」ケース、null、値の欠けもない。完璧に見える。だが、このコードは型チェックができない。
つまり以下の書き方も可能だ。

        var data = new Dictionary<Member.Key, object>()
        {
            { Member.IdKey, "123456" }
            { Member.AgeKey, "AAA" } // 本来整数なのに文字列でも入ってしまう
            { Member.NameKey, "AAA" }
        }
        new MemberManager().Update(data);

また、データ型定義を変更した場合もコンパイルエラーが出ないためメンテナンスには苦労すると思われる。

値の未設定/デフォルト値設定を分別できるオブジェクトにする

Dictionary のいいところを利用して、型のコンパイルエラーも検知したいという仕様。オブジェクトの皮を被ったDictionary を作る。
コード例を表すと。

    public class MemberManager
    {
        public void Create(Member member)
        {
            // 作成処理:member.Id のデータを作成。
            // member.Age が null なら、データを null 
            // member.Name が null なら、データを null
        }
        public void Update(Member member)
        {
            var dictionary = member.ToDictionary();
            // 更新処理:member.Id に該当するデータを更新という前提。
            if (!dictionary.ContainsKey(Member.AgeKey)) // Age を更新しない
            if (!dictionary.ContainsKey(Member.NameKey)) // Name を更新しない
        }
    }
    public class Member {
        // ハッシュキー
        public static readonly Key IdKey = new Key("Id");
        public static readonly Key AgeKey = new Key("Age");
        public static readonly Key NameKey = new Key("Name");
        public class Key {
            public string Value { get; }
            private Key(string value) { Value = value; }
        }

        // 内部データ
        private Dictionary<Member.Key, object> _dictionary = new Dictionary<Member.Key, object>();
        public Dictionary<Member.Key, object> ToDictionary() { return _dictionary; }

        // プロパティの動作挙動定義
        private T Get<T>(Member.Key key) {
            return _dictionary.containsKey(key) ? (T)_dictionary[key] : default(T);
        }
        private void Set<T>(Member.Key key, T value) {
            if (_dictionary.containsKey(key)) _dictionary[key] = value;
            else _dictionary.Add(key, value);
        }

        // _dictionary から値を取得、値を設定
        public string Id {
            get { return Get<string>(Member.IdKey); }
            set { Set(Member.IdKey, value); }
        }
        public int Age {
            get { return Get<int>(Member.AgeKey); }
            set { Set(Member.AgeKey, value); }
        }
        public string Name {
            get { return Get<string>(Member.NameKey); }
            set { Set(Member.NameKey, value); }
        }
    }

このコードを使う時は、下記のように使う。

        var data = new Member();
        data.Id = "123456";
        data.Age = 23;
        data.Name = "AAA"; //更新しない場合は記載しない。これで_dictionary には含まれないないので更新対象にならない。
        // data.Name = null; //null にしたい場合
        new MemberManager().Update(data);

これだと型チェックもできるし、定義していない/明示的にnullを設定したかを判断できる。

C#のドキュメントコメント要素をトリムする方法

概要

XML処理のめんどくさい問題にぶち当たったのでメモ。
ドキュメントコメントからXML解析しているときに各要素の最初・最後の空白文字がうまくトリムできなかった。
今回書くのは、その解決方法。

問題事象

まず、以下のように書いたらなんか表示がおかしい。

    // XMLのマッピングオブジェクト
    [XmlRoot(ElementName="member")]
    public class Class
    {
        [XmlElement(ElementName = "summary")]
        public string Summary { get; set; }

        [XmlElement(ElementName = "typeparam")]
        public List<TypeParam> TypeParams { get; set; }

    }
    
     // TはXMLの読み込み結果をどんな形式で返すかを決定する型
     public static T Parse<T>(string documentcomment)
     {
         using (Stream stream = new MemoryStream(Encoding.Unicode.GetBytes(documentcomment.Trim())))
         {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            return (T)serializer.Deserialize(stream);
         }
     }

     // 使い方
     public static void Main(string[] args)
     {
          Class klass = XMLParser.Parse<Class>(@"
 <member>
 <summary>
 抽象クラス.
 </summary>
 <typeparam name=""T"">格納オブジェクト型</typeparam>
 </member>");

           Console.WriteLine("サマリ:" + klass.Summary);
           Console.WriteLine("型パラメータ:" + klass.TypeParams[0].Name + " (" + klass.TypeParams[0].Value + ")");
      }

すると、サマリは以下のような感じに改行や空白が入る。

サマリ:
 抽象クラス.

型パラメータ:T (格納オブジェクト型)

原因・解決法

これは、 <summary>~</summary>の間に抽象クラス.の前後に空白文字があり、XmlSerializerはそれを保持するから。
代わりに型パラメータの方は、空白が入っていないため想定通りに出ている

各要素でトリムをしたいのだ。なんとかならんのか。が、XmlSerializer にはそれっぽいメソッドはない感じ。
マッピングオブジェクト作成後に各要素をトリムするという恐ろしくスマートでないやり方も・・・。

が、調べてるといいものが見つかった。
.net - XML Deserialization of string elements with newlines in C# - Stack Overflow

You can create custom XmlTextReader class:

public class CustomXmlTextReader : XmlTextReader
{
public CustomXmlTextReader(Stream stream) : base(stream) { }

public override string ReadString()
{
return base.ReadString().Trim();
}
}

いけた!

        private class CustomXmlTextReader : XmlTextReader
        {
            public CustomXmlTextReader(Stream stream) : base(stream) { }

            public override string ReadString()
            {
                return base.ReadString().Trim();
            }
        }

        public static T Parse<T>(string documentcomment)
        {

            using (Stream stream = new MemoryStream(Encoding.Unicode.GetBytes(documentcomment.Trim())))
            {
                var reader = new CustomXmlTextReader(stream);

                XmlSerializer serializer = new XmlSerializer(typeof(T));
                return (T)serializer.Deserialize(reader);
            }
        }
サマリ:抽象クラス.
型パラメータ:T (格納オブジェクト型)

C#でのExcel操作 セル操作編

概要

前回記事より。
C#でのExcel操作取得編 - Status Code 303 - See Other

例のごとくClosed XMLを使って遊んでみる。今回は、Excelのセル操作をC#で実現する方法のメモ。
中々文献が見つからないが、触ってみるとだんだんと使いやすさが分かってくる。

テスト用のコード

今回紹介するのはセル操作のみなので、A2のcell参照を取得して、それに操作してセーブする流れとする。

	// 編集用のファイルを C:\\Users\\hoshikouki\\sample.xlsx に置いている想定。
	using (var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx"))
	{
		var ws = book.Worksheet("Sheet1");
		var cell = ws.Cell("A2");

		//
		//ここに下記コード張って楽しんでください!
		//

		// セーブ
		book.SaveAs("C:\\Users\\hoshikouki\\sample2.xlsx");
	}

セル操作

データ入力規則(リスト)
	cell.DataValidation.List(ws.Range("$E$1:$E$4"));
コメント追加
	cell.Comment.AddText("コメント!");
ハイパーリンク設定

似たようなコンストラクタのオーバーロードがいっぱいある。

	cell.Hyperlink = new XLHyperlink("Sheet2!A1");
データ形式変更
         cell.DataType = XLCellValues.Text;
         //cell.DataType = XLCellValues.Number;
         //cell.DataType = XLCellValues.DateTime;
         //cell.DataType = XLCellValues.TimeSpan;
データ表示方法変更
	cell.Style.DateFormat.Format = "yyyyMMdd";
	// cell.Style.NumberFormat.Format = "(-###)";
文字色変更
	 cell.Style.Fill.BackgroundColor = XLColor.Gold;
	 //cell.Style.Fill.BackgroundColor = XLColor.NoColor;
背景色変更
	 cell.Style.Fill.BackgroundColor = XLColor.Gold;
	 //cell.Style.Fill.BackgroundColor = XLColor.NoColor;
セル周りの線
         cell.Style.Border.BottomBorder = XLBorderStyleValues.DashDot;
         cell.Style.Border.BottomBorderColor = XLColor.Red;
         // cell.Style.Border.OutsideBorder = XLBorderStyleValues.Thick;
         // cell.Style.Border.OutsideBorderColor = XLColor.Red;
セル内の表示場所
         // 垂直方向操作
         cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Bottom;
         // 水平方向操作
         cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right;
         // セル内に収まるように文字サイズを調整して表示
         cell.Style.Alignment.ShrinkToFit = true;
         // 折り返して全体を表示する
         cell.Style.Alignment.WrapText = true;

C#でのExcel操作取得編

概要

C#Excelを操作した調査内容。今回は、NuGetから取得できるライブラリClosedXMLを使ったメモ。

インストー

Nugetから「OpenXML」をインストール。

Excel操作インスタンス取得

ファイルを開いていると IOException が出るので、閉じてから実行すること。

	var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx");

ファイルを開くため当然閉じる処理も必要だが、IDisposableを実装しているため using 構文が使える。

	using(var book = new XLWorkbook("C:\\Users\\hoshikouki\\sample.xlsx"))
	{
		// 処理
	}

シート取得

	// ○番目のシート取得
	var sheet = book.Worksheet(1);
	// シート名で取得
	var sheet = book.Worksheet("Sheet1");	

存在しないシート名や存在しないシート数指定するとExceptionが発生する。
シート名指定の英大・小文字は特に区別しないらしく、"Sheet1"を"SHEET1"でも"SHEet1"でも取得できる。

セル取得

	// (行,列)番号で取得
	var cell = sheet.Cell(i, j);
	// 範囲文字列で取得
	var cell = sheet.Cell("A5");

行列の番号は1以上で指定する。0指定するとExceptionが発生する。

セル集合取得

セル集合に対する一括操作とかで使う。

	// 使用セル全部
	var cells = sheet.Cells();
	// 範囲文字列で取得
	var cells = sheet.Cell("A1:C11");
	// 条件でセルを取得(背景色が黄色のセル)
	var cells = sheet.Cells(x => x.Style.Fill.BackgroundColor == XLColor.Yellow);

条件でセルを取得する場合、全セルを調査しているせいかOutOfMemoryExceptionが発生する。あまりやらない方が良いかも。

セル値取得

	// 値や簡単な計算の場合の値取得
	var value = cell.Value;
	// 計算値などを取得する場合
	var cell = cell.ValueCached;

Valueプロパティで取得した場合、一部関数(ROW(), COLUMN()など)がサポートされていないようで例外が発生することがある。
ValueCachedプロパティはROW()、COLUMN()が動いた。けれど計算した値でなければNullになる。
このことから、以下のようにすればどちらでもいけそう。

	var value = cell.ValueCached ?? cell.Value;

計算式取得

	var formula = cell.FormulaA1;

なんでA1なんて名前がついているのか分からんけど、式は取得はできた。

C#のAOPライブラリ(Fody)

概要

前回記事(C#のAOPライブラリ(PostSharp) - Status Code 303 - See Other
上記とは別のライブラリ(無料版)を使ってAOPを試した内容。
GitHub - Fody/Fody: Extensible tool for weaving .net assemblies

使い方

サンプルクラス(PostSharpとほぼ同様)

全体処理は以下のようにする。

using System;

namespace FodySample
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Start Main");

			new Sample().Execute();

			Console.WriteLine("End Main");
			Console.Read();
		}
	}
}

今回は以下のSampleクラスのメソッドに対して、メソッド開始・終了にログを出力できるようにする。

using System;

namespace FodySample
{
	class Sample
	{
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		public string GetVal()
		{
			return "Fody";
		}
	}
}

上記プログラムは、以下の出力になる。

Start Main
Hello Fody!
End Main
Fodyインストー

NuGetから以下のパッケージをインストールする。

  • Fody (ver 1.29.4)
  • MethodDecorator.Fody (ver 0.9.0.6)

Fodyの安定版の最新は2.1.0(2017/6/25現在)だが、このバージョンはビルド時に以下のエラーが出て動かない。

Fody: The weaver assembly 'MethodDecorator.Fody, Version=0.9.0.4, Culture=neutral, PublicKeyToken=null' references an out of date version of Mono.Cecil.dll (cecilReference.Version). At least version 0.10 is expected. The weaver needs to add a NuGet reference to FodyCecil version 2.0.

検索したら何かしらエラーが出てきた。MonoCecilのバージョン違いが原因みたいだがどうすりゃ直るのか分からなかった。
Please fix this error · Issue #53 · Fody/MethodDecorator · GitHub
念のため、Fodyパッケージの参照するMono.Cecil.dllのバージョンを確認したが、0.10.0.0-beta6だったので問題なさそう。MethodDecorator側の問題?
・・こちらに関しては解決したら追記します。
今回は、1.29.4にダウングレードして動作を確認してます。

MethodDecorator.FodyはFodyに機能を組み込むアドインとして実装されているらしい。
そのため、Fodyにどのアドインを組み込むかを FodyWeavers.xml に定義しなければいけない。

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <MethodDecorator />
</Weavers>
AOP適用定義

AOP適用属性を定義。適用メソッドのログ出力。

using System;
using System.Reflection;

[module: FodySample.Logger]
namespace FodySample
{
	[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
	public class Logger : Attribute
	{
		private object instance;
		private MethodBase method;
		private object[] args;

		public virtual void Init(object instance, MethodBase method, object[] args)
		{
			this.instance = instance;
			this.method = method;
			this.args = args;
		}

		public void OnEntry()
		{
			Console.WriteLine("OnEntry {0}", method.Name);
		}

		public void OnExit()
		{
			Console.WriteLine("OnExit {0}", method.Name);
		}

		public void OnException(Exception exception)
		{
		}
	}
}
適用メソッドに属性付加
using System;

namespace FodySample
{
	class Sample
	{
		[Logger]
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		[Logger]
		public string GetVal()
		{
			return "Fody";
		}
	}
}

すると、AOP適用したプログラムは以下の出力になる。

Start Main
OnEntry Execute
OnEntry GetVal
OnExit GetVal
Hello Fody!
OnExit Execute
End Main

参考までに

MethodDecorator.Fodyの場合、コンパイルした後は以下のようになるらしい。
なお、以下はInterceptorAttributeをAOP適用属性を作った場合。

	public void Method(int value) {
	    InterceptorAttribute attribute = (InterceptorAttribute) Activator.CreateInstance(typeof(InterceptorAttribute));

		// in c# __methodref and __typeref don't exist, but you can create such IL
		MethodBase method = MethodBase.GetMethodFromHandle(__methodref (Sample.Method),__typeref (Sample));

		object[] args = new object[1] { (object) value };

		attribute.Init((object)this, method, args);

		attribute.OnEntry();
	    try {
	        Debug.WriteLine("Your Code");
	        attribute.OnExit();
	    }
	    catch (Exception exception) {
	        attribute.OnException(exception);
	        throw;
	    }
	}

コンソールアプリ内のSampleクラスをILSpyで逆コンパイルした結果は以下のようになった。

using System;
using System.Reflection;

namespace FodySample
{
	internal class Sample
	{
		[Logger]
		public void Execute()
		{
			MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(methodof(Sample.Execute()).MethodHandle, typeof(Sample).TypeHandle);
			Logger logger = (Logger)Activator.CreateInstance(typeof(Logger));
			object[] args = new object[0];
			logger.Init(this, methodFromHandle, args);
			logger.OnEntry();
			try
			{
				Console.WriteLine("Hello {0}!", this.GetVal());
				logger.OnExit();
			}
			catch (Exception exception)
			{
				logger.OnException(exception);
				throw;
			}
		}

		[Logger]
		public string GetVal()
		{
			MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(methodof(Sample.GetVal()).MethodHandle, typeof(Sample).TypeHandle);
			Logger logger = (Logger)Activator.CreateInstance(typeof(Logger));
			object[] args = new object[0];
			logger.Init(this, methodFromHandle, args);
			logger.OnEntry();
			string result;
			try
			{
				string text = "Fody";
				result = text;
				logger.OnExit();
			}
			catch (Exception exception)
			{
				logger.OnException(exception);
				throw;
			}
			return result;
		}
	}
}

PostSharpとの違い

OnExitの挙動が違う

PostSharpでは、AOP適用後のOnExitメソッドはfinally句にあったが、Fodyでは、try句の最後にあるため、以下のような違いになる。

  • PostSharp: AOP適用メソッドで例外が発生しようがOnExitメソッドが動く
  • Fody: AOP適用メソッドで例外が発生した場合はOnExitは実行されない

C#のAOPライブラリ(PostSharp)

AOP(Aspect Oriented Programming)?

ログ出力や例外処理など、メソッド全体に共有な処理を重複定義せず一か所に定義したいことがある。
このような共有処理を側面(Aspect)として定義した後、メソッドに適用する手法。

もし、全部それぞれにコピペなどで定義しまった場合。
その部分に修正が入った場合、全てのメソッドを直さないといけなくなり、保守性が低下する。
さらに、本来クラスにさせたい処理でないため、コードの見通しや可読性が低下する。

PostSharp

C#では有名なAOPライブラリ。本来は有料だが、Expressエディションを使えば適用数が制限されるが、無料で実行できる。
(プロジェクト単位:最大10個、ソリューション:最大50個)

なお、最上位版のPostSharp Ultimateを購入すると大体10万円かかるらしい。
PostSharp – the #1 pattern-aware extension to C# and VB

どんなとき使うの?

ドキュメントに記載されている、よくある使用法。

  • プロパティ値が変更されたときの通知
  • UndoとRedo
  • 契約プログラミング(事前条件/事後条件確認)
  • ログ・監視・プロファイリング
  • 例外発生時の制御
  • トランザクション制御
  • マルチスレッド時の挙動制御(バックグランド・フォアグランド制御、不変性・同期など)
  • セキュリティ(入力チェック、認証など)

使い方

サンプルクラス

全体処理は以下のようにする。

using System;

namespace PostSharpSample
{
	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Start Main");

			new Sample().Execute();

			Console.WriteLine("End Main");
			Console.Read();
		}
	}
}

今回は以下のSampleクラスのメソッドに対して、メソッド開始・終了にログを出力できるようにする。

using System;

namespace PostSharpSample
{
	class Sample
	{
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		public string GetVal()
		{
			return "PostSharp";
		}
	}
}

上記プログラムは、以下の出力になる。

Start Main
Hello PostSharp!
End Main
PostSharpインストー

NuGetからPostSharpをインストールする。なお、ビルド時に設定ウィンドウが現れるので、Expressを使う。

AOP属性定義

今回は、メソッド起動時および終了時に起動ログを標準出力する属性を定義する。

using PostSharp.Aspects;
using PostSharp.Serialization;
using System;

namespace PostSharpSample
{
	[PSerializable]
	class LoggerAttribute : OnMethodBoundaryAspect
	{

		public override void OnEntry(MethodExecutionArgs args)
		{
			Console.WriteLine("OnEntry {0}", args.Method.Name);
		}

		public override void OnExit(MethodExecutionArgs args)
		{
			Console.WriteLine("OnExit {0}", args.Method.Name);
		}
	}
}
適用メソッドに属性付加
using System;

namespace PostSharpSample
{
	class Sample
	{
		[Logger]
		public void Execute()
		{
			Console.WriteLine("Hello {0}!", GetVal());
		}

		[Logger]
		public string GetVal()
		{
			return "PostSharp";
		}
	}
}

すると、AOP適用したプログラムは以下の出力になる。

Start Main
OnEntry Execute
OnEntry GetVal
OnExit GetVal
Hello PostSharp!
OnExit Execute
End Main

参考までに

ドキュメントはこちら。PostSharp Documentation
今回の属性適用によって、ビルド時に適用されたメソッドは以下のようになるらしい。
参照:OnMethodBoundaryAspect Class

int MyMethod(object arg0, int arg1)
{
   OnEntry();
   try
   {
    // Original method body. 
    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

コンソールアプリ内のSampleクラスをILSpyを用いて逆コンパイルすると以下のようになっていた。

using PostSharp.Aspects;
using PostSharp.ImplementationDetails_f584e409;
using System;

namespace PostSharpSample
{
	internal class Sample
	{
		public void Execute()
		{
			MethodExecutionArgs methodExecutionArgs = new MethodExecutionArgs(null, null);
			<>z__a_1.a2.OnEntry(methodExecutionArgs);
			try
			{
				Console.WriteLine("Hello {0}!", this.GetVal());
			}
			catch (Exception exception)
			{
				methodExecutionArgs.Exception = exception;
				throw;
			}
			finally
			{
				<>z__a_1.a2.OnExit(methodExecutionArgs);
			}
		}

		public string GetVal()
		{
			MethodExecutionArgs methodExecutionArgs = new MethodExecutionArgs(null, null);
			<>z__a_1.a3.OnEntry(methodExecutionArgs);
			string result;
			try
			{
				string text = "PostSharp";
				result = text;
			}
			catch (Exception exception)
			{
				methodExecutionArgs.Exception = exception;
				throw;
			}
			finally
			{
				<>z__a_1.a3.OnExit(methodExecutionArgs);
			}
			return result;
		}
	}
}

この他にも、無料のAOPツールにFodyってのがあるらしいが、上手く動かなかったため後日再チャレンジ予定。

JUnitテスト前提条件制御

概要

毎回テストをするうえでお決まりの処理というのがある。
これらの処理をまとめる機構がテストフレームワークには大体存在する。
今回はJUnitの便利なテスト前提条件を設定する方法について記述する。

これらをやることによって、以下の利点がある。

  • テストがどんなものが分かりやすくなる
  • 処理が一か所にまとまる
  • テストの本質的な内容だけが目立つようになる

詳細説明

今回は、seleniumでサイトテストを作成しようとした例で説明する。
seleniumはブラウザを用いた機能ごとのテストや結合・シナリオテストで使われるライブラリだ。
なぜ、seleniumを用いたテストかというと、毎回初期化しないとあるテストが他のテストに影響しやすいからだ。

散在したテスト前提条件をまとめる

テストを実行するとき、前提条件・事後条件を定義したうえでのテストが多い。
一例としては、下記の内容だ。

  • 現在あるページにいること
  • ログインしていること

これらは、通常であれば JUnit が提供している @Before/@After を利用すれば事足りるが、
これが多数のテストクラスに必要だった場合、同じロジックが多数クラスに散在してしまう。

この問題を解決するために、@Rule というものがある。これは、@Before/@Afterをまとめる役割をする。
この@Ruleがつけられたフィールドをクラス内に配置すると、JUnitがテストの実行前後にロジックを実行してくれる。

public class LoginTest {

	/**
	 * テストルール.
	 */
	@Rule
	public WebTestRule rule = new WebTestRule();

	/**
	 * ブラウザ.
	 */
	Browser browser;

	@Before
	public void setUp() {
		browser = rule.getBrowser();
		browser.navigateTo(loginPage.getUrl());
	}

	@Test
	public void ログインテスト() throws Exception {
		User user = new User("ID", "PW");
		browser.getElement(loginPage.idText).sendKeys(user.getId());
		browser.getElement(loginPage.passwordText).sendKeys(user.getPassword());
		browser.getElement(loginPage.submitButton).click();

		assertThat(browser.getUrl(), is(new MemberPage().getUrl()));
	}

	@Test
	public void ログインが失敗すること() throws Exception {
		User user = new User("IDErr", "PWErr");
		browser.getElement(loginPage.idText).sendKeys(user.getId());
		browser.getElement(loginPage.passwordText).sendKeys(user.getPassword());
		browser.getElement(loginPage.submitButton).click();

		assertThat(browser.getUrl(), is(loginPage.getUrl()));
		assertThat(browser.getElement(loginPage.errorComment).getText(), is("認証情報が違います"));
	}
}

@Ruleがつけられたインスタンスの実装内容は以下のようになる。
ここでは、ブラウザをテスト前に開いて、テスト後に閉じている。

public class WebTestRule extends ExternalResource {

	protected Browser browser = new Browser();

	public Browser getBrowser() {
		return browser;
	}

	@Override
	protected void before() throws Throwable {
		browser.open();
	}

	@Override
	protected void after() {
		browser.close();
	}
}

最終的にこのテストでは、下記の順番でメソッドが動くことになる。

  1. @Ruleが付与されたフィールドのbeforeメソッド
  2. @Before が付与されたメソッド
  3. テスト実行
  4. @Ruleが付与されたフィールドのafter

@Ruleの内容によって、これがWebのテストであると分かるし、
その詳細なロジックはこのクラス内には記載されていないので、テスト内容だけが目立つようになる。

テスト実行前・実行後の挙動を追加

また、挙動の追加もそれほど難しくない。
例えば、selenium起動前後にさらにロジックを追加したい場合もあるだろう。
この場合、以下の2ステップで変更できる。

  1. 既存のRuleクラスを拡張したクラスを作成
  2. @Ruleを付与したフィールドを拡張したクラスインスタンスに置き換えるだけ。

例えば、seleniumでブラウザを起動した後、あるサイトにログインした状態を作ってから、テストをしたい場合があるとする。
その場合、WebTestRuleクラスを継承した以下のクラスを作成すればよい。

public class LoginRule extends WebTestRule {

	@Override
	protected void before() throws Throwable {
		super.before();
		LoginPage login = new LoginPage();
		super.browser.navigateTo(login.getUrl());
		super.browser.getElement(login.idText).sendKeys("ID");
		super.browser.getElement(login.passwordText).sendKeys("PW");
		super.browser.getElement(login.submitButton).click();
	}
}
テストクラス実行前と実行後のみ動かしたい場合

次に、beforeメソッド・afterメソッドの実行タイミング変更について記述する。
これまでのルールは、各テストごとにbeforeメソッド・afterメソッドが起動するため、
テストが多くなるとテスト時間が多くかかってしまう。(seleniumドライバの初期化処理が軽くないため)
これを軽減するためによく使われる方法として、全てのテストに共通のまたがった環境をつくる。

これは @Rule → @ClassRule に変更すれば可能であり、下記の動作になる。

  • 最初のテストの実行前 → beforeメソッドが起動
  • 最後のテストの実行後 → afterメソッドが起動
public class ログイン前状態 {

	/**
	 * テストルール.
	 */
	@ClassRule
	public WebTestRule rule = new WebTestRule();

(以下省略)

このテストは速度が向上する代わりに、あるテストが他のテストに影響してしまう危険性もある。
例えば、ログインした状態とそうでない状態で結果が全く異なる場合。

この場合、よく用いられるのは、ログイン処理→検証→ログアウト処理をテストメソッドに直接記述する方法。
なぜなら、それを共通の箇所(@Before/@Afterが付与されたメソッド)に書くと毎回起動されてしまうから。

しかし、これはテストとして間違っていると思う。本来テストには、テスト処理と検証だけがあるべきだ。
仮に末尾のログアウト処理で例外が発生した場合、それはこのテストの失敗であるといえるだろうか?

複数条件をテストクラスに記述する

この問題を解決するには、Enclosedテストランナーを使う。
これらは、別々の前提条件をテストクラスに複数含めることができる。

@RunWith(Enclosed.class)
public class LoginPageTest {

	/**
	 * テスト対象.
	 */
	static LoginPage loginPage = new LoginPage();

	public static class ログイン状態 {

		@Rule
		public LoginRule rule = new LoginRule();

		@Test
		public void メンバーページに遷移すること() throws Exception {
			rule.getBrowser().navigateTo(loginPage.getUrl());
			assertThat(browser.getUrl(), is(new MemberPage().getUrl()));

		}
	}

	public static class ログイン前状態 {

		@Rule
		public WebTestRule rule = new WebTestRule();

		Browser browser;

		@Before
		public void setUp() {
			browser = rule.getBrowser();
			browser.navigateTo(loginPage.getUrl());
		}

		@Test
		public void ログインテスト() throws Exception {
			User user = new User("ID", "PW");
			browser.getElement(loginPage.idText).sendKeys(user.getId());
			browser.getElement(loginPage.passwordText).sendKeys(user.getPassword());
			browser.getElement(loginPage.submitButton).click();

			assertThat(browser.getUrl(), is(new MemberPage().getUrl()));
		}

		@Test
		public void ログインが失敗すること() throws Exception {
			User user = new User("IDErr", "PWErr");
			browser.getElement(loginPage.idText).sendKeys(user.getId());
			browser.getElement(loginPage.passwordText).sendKeys(user.getPassword());
			browser.getElement(loginPage.submitButton).click();

			assertThat(browser.getUrl(), is(loginPage.getUrl()));
			assertThat(browser.getElement(loginPage.errorComment).getText(), is("登録されていませんよ?"));
		}
	}
}

最後に

今回は、テスト項目の制御という目的で @Ruleと@ClassRuleについて触れた。
これらがJUnitですっきりしたテストコードを書く上では大事なものであることはまちがいない。
テストがきれいであれば色んな人にも読みやすいし、これからテストを書く人にも敷居が低く感じてもらえるはずだ。