Status Code 303 - See Other

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

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ですっきりしたテストコードを書く上では大事なものであることはまちがいない。
テストがきれいであれば色んな人にも読みやすいし、これからテストを書く人にも敷居が低く感じてもらえるはずだ。

Visual Studio Community Edition でカバレッジ計測

概要

Visual Studioカバレッジを計測できるかと思ったら、結構大変だったのでメモ。

環境

OS
Windows7 SP1 64bit
IDE
Microsoft Visual Studio Community 2015 ver14.0.23107

プロダクト作成

プロジェクト作成

今回はプロジェクト名をSampleとする。
f:id:kouki_hoshi:20170520051925p:plain
f:id:kouki_hoshi:20170520051931p:plain

プロダクトコード作成

今回はCartクラスを作成する。

using System.Collections.Generic;

namespace Sample
{
	public class Cart
	{
		public List<Item> Items { get; }

		public Cart()
		{
			Items = new List<Item>();
		}

		public int Count
		{
			get { return Items.Count; }
		}

		public Item Get(int index)
		{
			return Items[index];
		}

		public void Add(Item item)
		{
			Items.Add(item);
		}
	}
}

ついでにカートの中に突っ込むItemクラスを作ってみる。

namespace Sample
{
	public class Item
	{
		public int price { get; set;  }

		public string name { get; set; }
	}
}

f:id:kouki_hoshi:20170520052007p:plain

テスト用プロジェクト作成

今回はテスト対象のテストがSampleなので、プロジェクト名を Sample.Test とする。
f:id:kouki_hoshi:20170520052017p:plain
f:id:kouki_hoshi:20170520052024p:plain

テスト用プロジェクトにカバレッジ用のライブラリを入れる
  • OpenCover
  • ReportGenerator

f:id:kouki_hoshi:20170520052049p:plain
f:id:kouki_hoshi:20170520052057p:plain

プロダクト参照をテストプロジェクトに設定

f:id:kouki_hoshi:20170520052306p:plain
f:id:kouki_hoshi:20170520052310p:plain

テストコード作成

今回はCartクラスに対してのみテストするため、名前をCartTestとする。なお、テストフレームワークVisual Studio 標準。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sample;

namespace SampleTest
{
	[TestClass]
	public class CartTest
	{

		Cart target;

		[TestInitialize]
		public void カート作成()
		{
			target = new Cart();
		}

		[TestMethod]
		public void 商品は空であること()
		{
			Assert.AreEqual(0, target.Items.Count);
		}

		[TestMethod]
		public void 商品が追加できること()
		{
			Item item = new Item();
			target.Add(item);

			Assert.AreEqual(1, target.Count);
			Assert.AreSame(item, target.Items[0]);
		}

	}
}

f:id:kouki_hoshi:20170520052339p:plain

カバレッジ用のバッチを作成する

OpenCover を使ってコードカバレッジを計測したメモ - present
基本的に上記を参考に作成。しかし、私の環境では、OpenCoverやReportGeneratorが上記と異なるところにある。NuGetで取得したから?
以下のバッチファイルを、今回はテストプロジェクトのルートに置いておく。

また、今回自動でレポートを開くようにするため、以下を参考にした。
参考:バッチファイルからURLをブラウザで開く - Qiita

REM ###### Settings ######

SET PROJECT_NAME=Sample
SET MS_TEST=C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\MSTest.exe

SET REPORT_NAME=result.xml
SET OUTPUT_DIR=.\html

SET OPEN_COVER=.\packages\OpenCover.4.6.519\tools\OpenCover.Console.exe
SET REPORT_GEN=.\packages\ReportGenerator.2.5.8\tools\ReportGenerator.exe

SET TEST=.\%PROJECT_NAME%.Test\bin\Debug\%PROJECT_NAME%.Test.dll
SET COVERAGE_DIR=.\%PROJECT_NAME%\bin\Debug\
SET FILTERS=+[%PROJECT_NAME%]*

REM #######################

call :EXECUTE "%TEST%"
start "" %OUTPUT_DIR%\index.htm

exit

:EXECUTE

%OPEN_COVER% -register:user -target:"%MS_TEST%" -targetargs:"/noisolation /testcontainer:\"%~f1\"" -targetdir:%COVERAGE_DIR% -filter:"%FILTERS%" -output:%REPORT_NAME% -mergebyhash
%REPORT_GEN% "%REPORT_NAME%" %OUTPUT_DIR%

exit /b

設定項目は環境に合わせて変更する。

PROJECT_NAME
プロダクトのプロジェクト名。
MS_TEST
Microsoftの提供するテストアプリケーションの場所。バージョンやOSのビットによって変化するかも。
REPORT_NAME
デフォルトで特に動作に支障なし。レポート(xml)の出力先を指定する。
OUTPUT_DIR
デフォルトで特に動作に支障なし。レポート(htm)の出力先を指定する。
OPEN_COVER
OpenCoverのバージョンによって変わるかも。
TEST
テスト用dllのパス。デフォルト設定のDebugで生成されるパスを指定してある。異なる場所にあるなら変更。
COVERAGE_DIR
カバレッジを計るdllがあるディレクトリパス。デフォルト設定のDebugで生成されるパスを指定してある。異なる場所にあるなら変更。
FILTERS
カバレッジを計測するdllの条件を記載。

f:id:kouki_hoshi:20170520052428p:plain
f:id:kouki_hoshi:20170520052437p:plain
f:id:kouki_hoshi:20170520052439p:plain

なお、Visual Studio で保存すると、なぜかBOM付きで保存されるので、テキストエディタでBOMを取らないと動かない。
上記は、UTF-8 BOMなし形式で動作確認している。

バッチ仕様

このバッチは、XXXがプロダクトのプロジェクト名だとすると、XXX.Testをテストプロジェクトとするように定めている。
参照dllはプロダクト・テストともDebugで出力されるものを使用する。

使い方

プロジェクトのビルド

最終的にdllを見ることになるので、両プロジェクトをビルドしておく。
f:id:kouki_hoshi:20170520052519p:plain

パッケージマネージャー コンソールで以下のバッチコマンドを実行する
PS> .\Sample.Test\coverage.bat

f:id:kouki_hoshi:20170520052540p:plain

以下のようなレポートが表示されれば成功。
f:id:kouki_hoshi:20170520052556p:plain

NUnitの場合

NUnit.Console を NuGetから取得して、
バッチの2行を変更する。

# 4行目
SET MS_TEST=C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\MSTest.exe
# 25行目
%OPEN_COVER% -register:user -target:"%MS_TEST%" -targetargs:"/noisolation /testcontainer:\"%~f1\"" -targetdir:%COVERAGE_DIR% -filter:"%FILTERS%" -output:%REPORT_NAME% -mergebyhash

  ↓

# 4行目
SET NUNIT=.\packages\NUnit.ConsoleRunner.3.6.1\tools\nunit3-console.exe
# 25行目
%OPEN_COVER% -register:user -target:"%NUNIT%" -targetargs:"\"%~f1\"" -targetdir:%COVERAGE_DIR% -filter:"%FILTERS%" -output:%REPORT_NAME% -mergebyhash

これだと Visual Studio のインストール場所が依存しなくなるかも?

課題

事前にビルドしないとソースコードと異なる結果が出て勘違いするかもしれないので、ビルドしてから実行させる方がいい。
現状では、カバレッジに失敗してもエラー処理がないので、カバレッジ情報失敗しても最後まで処理が走ってしまう。

最後に

正直、色々なところで躓いた。まず、文献がそれほどないこと。
Visual Studio Ultimate 以上なら使えるらしいが、そうすると Professional に比べて値段がおよそ3倍以上になること。

OpenCoverをGUIで動作させることができるという拡張プラグイン(OpenCover UI)が動作しなかったこと。
バッチファイルの書き方がよく分からなかったり、Visual Studioでのバッチ起動がよく分からなかったり。
(パッケージマネージャーコンソールからの実行で本当にいいのか?^^;)

などなど。ただ、いい勉強にはなりました!