テスト、TDD原則、単体テストなど。
個人的にテストすることは多くの考えのトピックです。 テストは必要ですか? しかし、テストを記述するためにリソースが必要であると主張する人はいません。
次の2つのケースを検討してください。
- 私たちはサイトを作成し、顧客に見せ、彼は不正確さと追加の希望のリストを送信し、それらを元気に編集してサイトを顧客に与えます。 彼のサーバーに置きます。 誰も彼のサーバーに行きません、顧客は奇跡が起こらなかったことを理解して、ホスティング/ドメインの支払いを止めます。 サイトは死にかけています。 そこでテストが必要ですか?
- サイトを作成し、顧客に見せ、編集リストを送信し、元気に編集してサイトを立ち上げます。 6か月後、サイトには1日あたり300の一意のエントリがあり、この数字は日々増加しています。 顧客は常に新しい機能を要求し、古いコードが成長し始め、時間の経過とともに保守が難しくなります。
ご覧のとおり、ここでのジレンマは、サイトの立ち上げの結果が予測できないことです。 膝の上に作られたサイトが非常に元気に立ち上がったのも私と一緒で、顧客がクールな仕事にお金を払ったが、それを受け入れさえしなかったことが起こりました。 したがって、行動の戦術は次のようになります。
- 常にテストを作成します。 私たちはクールな会社です。あらゆる種類のテストでコードの90%をカバーしています。結果が完全に予測可能であり、一般的にハンサムなので、100500倍の時間/お金を費やすことは本当に重要ではありません。
- これまでにテストを作成しないでください。 私たちはクールな会社であり、私たちは頭の中でプロジェクト全体を再構築できるほど完璧に働いています。コードがコンパイルされていれば、それは完全に機能していることを意味します。 何かが機能しない場合は、おそらくホスティングであるか、ブラウザでエラーが発生しています。 またはそのような機能。
- テストを作成しますが、常にではありません。 ここでは、サイトやプロジェクトが何であれ、機能で構成されていることを理解する必要があります。 これは、ユーザーにあらゆる種類の機会と重要な機会を提供する必要があることを意味します。私は批判的であり、何らかの形でサイトに登録し、注文し、ニュースやコメントを追加します。 必要なときは不快ですが、サイトが必要なので登録できません。
- テストは何に使用されますか? これは、会計における複式記入の原則に似ています。 各アクション、各機能は、サイトのパフォーマンスだけでなく、少なくとも1つのテストによってチェックされます。 コードを変更する場合、ユニットテストはコードが間違っていることを示し、違反が発生した場所を強調表示します。 しかし、そうですか?
TDDの原理を検討してください。
- 課題を読み、失敗するテストを書きます
- このテストと他のテストに合格できるコードを作成します
- リファクタリング、つまり 必要に応じて重複コードを削除しますが、すべてのテストに合格します
たとえば、次の修正が行われました。
ブログにタグフィールドを追加することにしました。 すでに多くのブログエントリがあるため、このフィールドをオプションにすることにしました。 既存のコードがあるため、足場は使用しませんでした。 手動でレコードの作成を確認しました-すべて問題ありません。 テストを実行します-すべて問題ありません。 しかし、フィールドの変更をUpdatePostに追加するのを忘れていました(cache.Tags = instance.Tags;)。 古いレコードを変更するとき、実際に保存されないタグを追加します。 このテストではバタンと合格しました。 人生は苦痛です!
ご覧のとおり、TDDの基本原則に違反しました。最初に失敗するテストを記述し、次にそれを処理するコードを記述します。 しかし(!)ここには2つ目のトリックがあります-タグ付きのブログエントリの作成をチェックするテストを作成しました。 もちろん、これはすぐにはコンパイルされませんでした(つまり、テストはパスしませんでした)が、ModelViewにthrow New NotImplementedException()のようなものを追加しました。 コンパイルされたものはすべて、テストは赤で点灯します。このフィールドにタグを追加し、例外を削除して、テストに合格します。 他のすべてのテストも合格します。 原則は尊重されますが、エラーは残ります。
すべての原則について、それが機能しない状況があります。 つまり そのようなことはありません-彼らは脳をオフにし、運転しました。 1つ確かなことは、これがこれらの考慮事項からの主な結論です。
テストは迅速に書かれるべきです
そのため、主にサイトでどのようなタスクを解決しますか:
- 情報を追加する
- 情報の検証
- 情報を変更する
- 情報を削除する
- 行動の権利の検証
- 情報出力
これらが主なアクションです。 たとえば、登録方法は次のとおりです。
- 入力するフィールドを表示します
- 「登録」をクリックすると、データが検証されます
- すべてが成功した場合、「Well done」ページを提供します。すべてが正常でない場合は、警告を発行し、監視の修正を許可します
- すべてが順調であれば、データベースにレコードがあります
- そして、アクティベーションレターも送ります
これらすべての単体テストを作成しましょう。
- 入力するフィールドを彼に何を示しますか(つまり、RegisterUserViewクラスの空のオブジェクトを渡します)
- 属性を持っていること、そしてデータベースに何を書き込むことができるかを本当に確認することを確認します
- 正確に「よくやった」ページを発行する
- レコードが表示され、2つのレコードがあり、3つのレコードがあったこと
- 何かを送信しようとしているので、テンプレートを見つけてMailNotifyを呼び出します。
それに取り掛かろう。
NUnitをインストールする
リンクhttp://sourceforge.net/projects/nunit/に従って、NUnitをインストールします。 NUnit Test AdapterもVSにインストールします(VSでテストを直接実行するため)。

タイプSolution Folder Testのフォルダーを作成し、それにLessonProject.UnitTestプロジェクトを追加して、そこにNUnitをインストールします。
Install-Package NUnit
(/Test/Default/UserContoller.cs)にUserControllerTestクラスを作成します。
[TestFixture] public class UserControllerTest { }
そのため、テストメソッドMethod_Scenario_ExpectedBehaviorの名前を記述する原則:
- メソッド-テストするメソッド[またはプロパティ]
- シナリオ-テストするスクリプト
- ExpectedBehavior-期待される動作
たとえば、登録のためにUserViewクラスでViewを最初に返すことを確認します。
public void Register_GetView_ItsOkViewModelIsUserView() { Console.WriteLine("=====INIT======"); var controller = new UserController(); Console.WriteLine("======ACT======"); var result = controller.Register(); Console.WriteLine("====ASSERT====="); Assert.IsInstanceOf<ViewResult>(result); Assert.IsInstanceOf<UserView>(((ViewResult)result).Model); }
したがって、すべてのテストは3つの部分に分かれていますInit-> Act-> Assert:
- Init-初期化、UserControllerを取得します
- Act-アクション、コントローラーを実行します。
- アサート-すべてが本当にそうであることを確認します。
[テストエクスプローラー]タブを開きます。

NUnitアダプターが正しくインストールされていれば、テストメソッドが表示されます。
始めます。 テストに合格すると、シャンパンを開けることができます。 うんこ。 これは最も簡単な部分にすぎませんが、何かを保存する部分についてはどうでしょう。 この場合、データベースはありません。リポジトリはnull、ゼロ、何もありません。
次に、初期化(ドキュメント)のクラスとメソッドを学習します。 SetUpFixture-この属性でマークされたクラスは、テスト前に初期化し、テスト後にクリーンアップするメソッドがあることを意味します。 これは同じ名前空間に適用されます。
- セットアップ-この属性でマークされたメソッドは、すべてのテストメソッドが実行される前に呼び出されます。 TestFixture属性を持つクラスにある場合、このクラスのメソッドのみが実行される前に呼び出されます。
- TearDown-この属性でマークされたメソッドは、すべてのテストが完了した後に呼び出されます。 TestFixture属性を持つクラスにある場合、すべてのメソッドが実行された後に呼び出されます。
クラスUnitTestSetupFixture.cs(/Setup/UnitTestSetupFixture.cs)を作成します。
[SetUpFixture] public class UnitTestSetupFixture { [SetUp] public void Setup() { Console.WriteLine("==============="); Console.WriteLine("=====START====="); Console.WriteLine("==============="); } [TearDown] public void TearDown() { Console.WriteLine("==============="); Console.WriteLine("=====BYE!======"); Console.WriteLine("==============="); } }
実行して取得:
===============
=====START=====
===============
=====INIT======
======ACT======
====ASSERT=====
===============
=====BYE!======
===============
モック
そのため、Mockはパロディオブジェクトです。 つまり たとえば、データベースではなく、データベースに似たものです。 ミラージュ、一般的に。 スタブもあります-これはスタブです。 スタブメソッドの例:
public int GetRandom() { return 4; }
ただし、Mockを使用します。
Install-Package Moq
使用する環境を定義して、Mockオブジェクトを初期化します。 基本的に、これがかつてNinject Kernelにもたらしたすべてです。
- IRepository
- Iconfig
- IMapper
- IAuthentication
そして、私は小さな発言をします。 ConfigをMirageオブジェクトにレンダリングすることはできません。 これが完全に不可能であるという計画ではなく、計画で-これは悪い仕事です。 たとえば、string.Format()がFormatExceptionエラーをスローするようにレターテンプレートを変更しました。 そして、テストでは、すべてが正常であり、テストは正常に合格します。 そして、彼はその後何に責任がありますか? まさか。 したがって、構成ファイルは元のまま使用する必要があります。 あとで残しておきましょう。
IMapperに関しては、これは必要ありません。CommonMapperを安全に使用できます。
ただし、最初に、テストモードで動作するようにIKernelを初期化します。 App_Start / NinjectWebCommon.csでは、RegisterServicesメソッドで、インターフェイスの実装方法を指定し、bootstrapper.Initialize(CreateKernel)で呼び出します。 将来的には、DependencyResolver.GetService()を介してサービスを受け取ることになります。 したがって、NinjectDependencyResolver(/Tools/NinjectDependencyResolver.cs)を作成します。
public class NinjectDependencyResolver : IDependencyResolver { private readonly IKernel _kernel; public NinjectDependencyResolver(IKernel kernel) { _kernel = kernel; } public object GetService(Type serviceType) { return _kernel.TryGet(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { try { return _kernel.GetAll(serviceType); } catch (Exception) { return new List<object>(); } } }
SetUp(/Setup/UnitTestSetupFixture.cs)にメソッドを追加します。
[SetUp] public virtual void Setup() { InitKernel(); } protected virtual IKernel InitKernel() { var kernel = new StandardKernel(); DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel)); InitRepository(kernel); // return kernel; }
MockRepositoryを作成する
(/Mock/Repository/MockRepository.cs):
public partial class MockRepository : Mock<IRepository> { public MockRepository(MockBehavior mockBehavior = MockBehavior.Strict) : base(mockBehavior) { GenerateRoles(); GenerateLanguages(); GenerateUsers(); } }
(/Mock/Repository/Entity/Language.cs)
namespace LessonProject.UnitTest.Mock { public partial class MockRepository { public List<Language> Languages { get; set; } public void GenerateLanguages() { Languages = new List<Language>(); Languages.Add(new Language() { ID = 1, Code = "en", Name = "English" }); Languages.Add(new Language() { ID = 2, Code = "ru", Name = "" }); this.Setup(p => p.Languages).Returns(Languages.AsQueryable()); } } }
(/Mock/Repository/Entity/Role.cs)
public partial class MockRepository { public List<Role> Roles { get; set; } public void GenerateRoles() { Roles = new List<Role>(); Roles.Add(new Role() { ID = 1, Code = "admin", Name = "Administrator" }); this.Setup(p => p.Roles).Returns(Roles.AsQueryable()); } }
(/Mock/Repository/Entity/User.cs)
public partial class MockRepository { public List<User> Users { get; set; } public void GenerateUsers() { Users = new List<User>(); var admin = new User() { ID = 1, ActivatedDate = DateTime.Now, ActivatedLink = "", Email = "admin", FirstName = "", LastName = "", Password = "password", LastVisitDate = DateTime.Now, }; var role = Roles.First(p => p.Code == "admin"); var userRole = new UserRole() { User = admin, UserID = admin.ID, Role = role, RoleID = role.ID }; admin.UserRoles = new EntitySet<UserRole>() { userRole }; Users.Add(admin); Users.Add(new User() { ID = 2, ActivatedDate = DateTime.Now, ActivatedLink = "", Email = "chernikov@gmail.com", FirstName = "Andrey", LastName = "Chernikov", Password = "password2", LastVisitDate = DateTime.Now }); this.Setup(p => p.Users).Returns(Users.AsQueryable()); this.Setup(p => p.GetUser(It.IsAny<string>())).Returns((string email) => Users.FirstOrDefault(p => string.Compare(p.Email, email, 0) == 0)); this.Setup(p => p.Login(It.IsAny<string>(), It.IsAny<string>())).Returns((string email, string password) => Users.FirstOrDefault(p => string.Compare(p.Email, email, 0) == 0)); } }
Mockの仕組みを見てみましょう。 彼はセットアップなどの良い方法を持っています(再び?!しっかりしたセットアップ!)、これは次のように動作します:
this.Setup( ).Returns( );
例:
this.Setup(p => p.WillYou()).Returns(true);
他のオプションがどんなものであるかをより詳細に検討しましょう:
- 方法
var mock = new Mock<IFoo>(); mock.Setup(foo => foo.DoSomething("ping")).Returns(true);
- 出力パラメータ
var outString = "ack"; mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);
- 参照パラメータ
var instance = new Bar(); mock.Setup(foo => foo.Submit(ref instance)).Returns(true);
- 入力パラメーターと戻り値への依存(いくつかのパラメーターが可能です)
mock.Setup(x => x.DoSomething(It.IsAny<string>())) .Returns((string s) => s.ToLower());
- 例外を投げる
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>(); mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command");
- (???)とコールバックを使用して異なる値を返します
var mock = new Mock<IFoo>(); var calls = 0; mock.Setup(foo => foo.GetCountThing()) .Returns(() => calls) .Callback(() => calls++);
- 出力パラメータ
- 一致する引数
- 任意の値
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Returns(true);
- Func <bool、T>を介した条件
mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
- 範囲
mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns(true);
- 正規表現
mock.Setup(x => x.DoSomething(It.IsRegex("[ad]+", RegexOptions.IgnoreCase))).Returns("foo");
- 任意の値
- プロパティ
- すべての財産
mock.Setup(foo => foo.Name).Returns("bar");
- 階層プロパティ
mock.Setup(foo => foo.Bar.Baz.Name).Returns("baz");
- すべての財産
- コールバック
- パラメータなし
mock.Setup(foo => foo.Execute("ping")) .Returns(true) .Callback(() => calls++);
- パラメータ付き
mock.Setup(foo => foo.Execute(It.IsAny<string>())) .Returns(true) .Callback((string s) => calls.Add(s));
- パラメーターを使用すると、わずかに異なる構文
mock.Setup(foo => foo.Execute(It.IsAny<string>())) .Returns(true) .Callback<string>(s => calls.Add(s));
いくつかのパラメーター
mock.Setup(foo => foo.Execute(It.IsAny<int>(), It.IsAny<string>())) .Returns(true) .Callback<int, string>((i, s) => calls.Add(s));
呼び出しの前後
mock.Setup(foo => foo.Execute("ping")) .Callback(() => Console.WriteLine("Before returns")) .Returns(true) .Callback(() => Console.WriteLine("After returns"));
チェック(モックオブジェクトはパラメーターの呼び出し回数を保存するため、コードが正しく実行されたかどうかも確認できます)
- 通常のチェックは、Executeメソッドが「ping」パラメーターで呼び出されたことです。
mock.Verify(foo => foo.Execute("ping"));
- 独自のエラーメッセージを追加
mock.Verify(foo => foo.Execute("ping"), "When doing operation X, the service should be pinged always");
- 一度呼び出されるべきではなかった
mock.Verify(foo => foo.Execute("ping"), Times.Never());
- 少なくとも一度は呼び出さなければなりませんでした
mock.Verify(foo => foo.Execute("ping"), Times.AtLeastOnce()); mock.VerifyGet(foo => foo.Name);
- プロパティのセッターが呼び出されている必要があります
mock.VerifySet(foo => foo.Name);
- 値「foo」のセッターが呼び出されている必要があります
mock.VerifySet(foo => foo.Name = "foo");
- セッターは、指定された範囲の値で呼び出されている必要があります。
mock.VerifySet(foo => foo.Value = It.IsInRange(1, 5, Range.Inclusive));
さて、これでこれで十分です。残りはここで読むことができます:
https://code.google.com/p/moq/wiki/QuickStart
UnitTestSetupFixture.cs(/Setup/UnitTestSetupFixture.cs)に戻り、構成を初期化します。
protected virtual void InitRepository(StandardKernel kernel) { kernel.Bind<MockRepository>().To<MockRepository>().InThreadScope(); kernel.Bind<IRepository>().ToMethod(p => kernel.Get<MockRepository>().Object); }
クラス、デフォルト、コントローラー、UserControllerなどの出力の一部を確認しましょう:cs:
[Test] public void Index_GetPageableDataOfUsers_CountOfUsersIsTwo() { //init var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>(); //act var result = controller.Index(); Assert.IsInstanceOf<ViewResult>(result); Assert.IsInstanceOf<PageableData<User>>(((ViewResult)result).Model); var count = ((PageableData<User>)((ViewResult)result).Model).List.Count(); Assert.AreEqual(2, count); }
BaseController.cs(/LessonProject/Controllers/BaseController.cs)で、Auth
およびConfig
プロパティからInject属性を削除します(そうしないと、選択した行はコントローラーを初期化できず、nullを返します)。 ハイライトされた行といえば。 すべてのInject-attributeプロパティが初期化されるように、この初期化を行います。 開始しますが、count == 2です。素晴らしい、MockRepositoryは動作します。Inject
属性を元に戻します。
ちなみに、テストは通常デバッグモードでは実行されません。デバッグを実行するには、これを行う必要があります。
それでは、Configを操作しましょう。 それは素晴らしいでしょう!
Testconfig
何をする必要がありますか。 必要なもの:
- LessonProjectプロジェクトからWeb.Configを取得する(トリッキーな方法で)
- そして、それに基づいて、IConfigインターフェイスを実装する特定のクラスを作成します
- さて、Ninject Kernelにフックする
- そして、あなたはそれを使うことができます。
始めましょう。 Web.Configを取得するには、フォルダーにコピーする必要があります。 サンドボックスと呼びましょう。 コピーして、プロジェクトプロパティのビルド前イベントに配置します。
xcopy $(SolutionDir)LessonProject\Web.config $(ProjectDir)Sandbox\ /y
ビルドを開始するたびに、SandboxでWeb.configをコピーします(必要に応じて上書きします)。
TestConfig.csを作成して、ファイル(/Tools/TestConfig.cs)をコンストラクターに転送します。
public class TestConfig : IConfig { private Configuration configuration; public TestConfig(string configPath) { var configFileMap = new ExeConfigurationFileMap(); configFileMap.ExeConfigFilename = configPath; configuration = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None); } public string ConnectionStrings(string connectionString) { return configuration.ConnectionStrings.ConnectionStrings[connectionString].ConnectionString; } public string Lang { get { return configuration.AppSettings.Settings["Lang"].Value; } } public bool EnableMail { get { return bool.Parse(configuration.AppSettings.Settings["EnableMail"].Value); } } public IQueryable<IconSize> IconSizes { get { IconSizesConfigSection configInfo = (IconSizesConfigSection)configuration.GetSection("iconConfig"); if (configInfo != null) { return configInfo.IconSizes.OfType<IconSize>().AsQueryable<IconSize>(); } return null; } } public IQueryable<MimeType> MimeTypes { get { MimeTypesConfigSection configInfo = (MimeTypesConfigSection)configuration.GetSection("mimeConfig"); return configInfo.MimeTypes.OfType<MimeType>().AsQueryable<MimeType>(); } } public IQueryable<MailTemplate> MailTemplates { get { MailTemplateConfigSection configInfo = (MailTemplateConfigSection)configuration.GetSection("mailTemplatesConfig"); return configInfo.MailTemplates.OfType<MailTemplate>().AsQueryable<MailTemplate>(); } } public MailSetting MailSetting { get { return (MailSetting)configuration.GetSection("mailConfig"); } } public SmsSetting SmsSetting { get { return (SmsSetting)configuration.GetSection("smsConfig"); } } }
UnitTestSetupFixture.cs(/Setup/UnitTestSetupFixture.cs)で初期化します。
protected virtual void InitConfig(StandardKernel kernel) { var fullPath = new FileInfo(Sandbox + "/Web.config").FullName; kernel.Bind<IConfig>().ToMethod(c => new TestConfig(fullPath)); }
設定内のデータをチェックする簡単なテストを作成しましょう:
[TestFixture] public class MailTemplateTest { [Test] public void MailTemplates_ExistRegisterTemplate_Exist() { var config = DependencyResolver.Current.GetService<IConfig>(); var template = config.MailTemplates.FirstOrDefault(p => p.Name.StartsWith("Register")); Assert.IsNotNull(template); } }
開始、確認、出来上がり! IAuthenticationの実装に渡します。
認証
Webアプリケーションでは、コントローラーでコードを既に実行しているとき、特定のコンテキスト(httpリクエストによって形成される環境)が既にあります。 つまり これらは、パラメータ、Cookie、ブラウザのバージョンデータ、画面解像度、およびオペレーティングシステムです。 全体として、これはHttpContextです。 承認中に一部のデータをCookieに入れてからすべて取得することを理解しておく必要があります。 実際には、このために、Cookieを入力する特別なIAuthCookieProviderインターフェイスを作成します
IAuthCookieProvider.cs(LessonProject / Global / Auth / IAuthCookieProvider):
public interface IAuthCookieProvider { HttpCookie GetCookie(string cookieName); void SetCookie(HttpCookie cookie); }
そして、HttpAuthCookieProvider.cs(/Global/Auth/HttpAuthCookieProvider.cs)に実装します。
public class HttpContextCookieProvider : IAuthCookieProvider { public HttpContextCookieProvider(HttpContext HttpContext) { this.HttpContext = HttpContext; } protected HttpContext HttpContext { get; set; } public HttpCookie GetCookie(string cookieName) { return HttpContext.Request.Cookies.Get(cookieName); } public void SetCookie(HttpCookie cookie) { HttpContext.Response.Cookies.Set(cookie); } }
そして、この実装を使用して、CustomAuthentication(/Global/Auth/CustomAuthentication.cs)のCookieを操作します。
public IAuthCookieProvider AuthCookieProvider { get; set; }
HttpContext.Request.Cookies.Getの代わりにGetCookie()を使用し、
HttpContext.Response.Cookies.Set-それぞれSetCookie()。
IAuthencation.cs(/Global/Auth/IAuthencation.cs)でも変更します。
public interface IAuthentication { /// <summary> /// ( ) /// </summary> IAuthCookieProvider AuthCookieProvider { get; set; }
そしてAuthHttpModule.cs(/Global/Auth/AuthHttpModule.cs)で:
var auth = DependencyResolver.Current.GetService<IAuthentication>(); auth.AuthCookieProvider = new HttpContextCookieProvider(context);
MockHttpContext
LessonProject.UnitTestでHttpContextのモックオブジェクトを作成します。
MockHttpContext.cs (/Mock/HttpContext.cs): public class MockHttpContext : Mock<HttpContextBase> { [Inject] public HttpCookieCollection Cookies { get; set; } public MockHttpCachePolicy Cache { get; set; } public MockHttpBrowserCapabilities Browser { get; set; } public MockHttpSessionState SessionState { get; set; } public MockHttpServerUtility ServerUtility { get; set; } public MockHttpResponse Response { get; set; } public MockHttpRequest Request { get; set; } public MockHttpContext(MockBehavior mockBehavior = MockBehavior.Strict) : this(null, mockBehavior) { } public MockHttpContext(IAuthentication auth, MockBehavior mockBehavior = MockBehavior.Strict) : base(mockBehavior) { //request Browser = new MockHttpBrowserCapabilities(mockBehavior); Browser.Setup(b => b.IsMobileDevice).Returns(false); Request = new MockHttpRequest(mockBehavior); Request.Setup(r => r.Cookies).Returns(Cookies); Request.Setup(r => r.ValidateInput()); Request.Setup(r => r.UserAgent).Returns("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11"); Request.Setup(r => r.Browser).Returns(Browser.Object); this.Setup(p => p.Request).Returns(Request.Object); //response Cache = new MockHttpCachePolicy(MockBehavior.Loose); Response = new MockHttpResponse(mockBehavior); Response.Setup(r => r.Cookies).Returns(Cookies); Response.Setup(r => r.Cache).Returns(Cache.Object); this.Setup(p => p.Response).Returns(Response.Object); //user if (auth != null) { this.Setup(p => p.User).Returns(() => auth.CurrentUser); } else { this.Setup(p => p.User).Returns(new UserProvider("", null)); } //Session State SessionState = new MockHttpSessionState(); this.Setup(p => p.Session).Returns(SessionState.Object); //Server Utility ServerUtility = new MockHttpServerUtility(mockBehavior); this.Setup(p => p.Server).Returns(ServerUtility.Object); //Items var items = new ListDictionary(); this.Setup(p => p.Items).Returns(items); } }
これに加えて、次のようなクラスを作成します。
- MockHttpCachePolicy
- MockHttpBrowserCapabilities
- MockHttpSessionState
- MockHttpServerUtility
- MockHttpResponse
- MockHttpRequest
セッションストレージが保存されるMockSessionState(/Mock/Http/MockHttpSessionState.cs)を除き、これらすべてのモックオブジェクトは非常に簡単です。
public class MockHttpSessionState : Mock<HttpSessionStateBase> { Dictionary<string, object> sessionStorage; public MockHttpSessionState(MockBehavior mockBehavior = MockBehavior.Strict) : base(mockBehavior) { sessionStorage = new Dictionary<string, object>(); this.Setup(p => p[It.IsAny<string>()]).Returns((string index) => sessionStorage[index]); this.Setup(p => p.Add(It.IsAny<string>(), It.IsAny<object>())).Callback<string, object>((name, obj) => { if (!sessionStorage.ContainsKey(name)) { sessionStorage.Add(name, obj); } else { sessionStorage[name] = obj; } }); } }
FakeAuthCookieProvider.cs(/Fake/FakeAuthCookieProvider.cs)を作成します。
public class FakeAuthCookieProvider : IAuthCookieProvider { [Inject] public HttpCookieCollection Cookies { get; set; } public HttpCookie GetCookie(string cookieName) { return Cookies.Get(cookieName); } public void SetCookie(HttpCookie cookie) { if (Cookies.Get(cookie.Name) != null) { Cookies.Remove(cookie.Name); } Cookies.Add(cookie); } }
ふう! UnitTestSetupFixture.cs(/Setup/UnitTestSetupFixture.cs)でこれを初期化します。
protected virtual void InitAuth(StandardKernel kernel) { kernel.Bind<HttpCookieCollection>().To<HttpCookieCollection>(); kernel.Bind<IAuthCookieProvider>().To<FakeAuthCookieProvider>().InSingletonScope(); kernel.Bind<IAuthentication>().ToMethod<CustomAuthentication>(c => { var auth = new CustomAuthentication(); auth.AuthCookieProvider = kernel.Get<IAuthCookieProvider>(); return auth; }); }
BindはSingletonScope()で発生することに注意してください。 あるテストにログインすると、後続のテストで同じ認証を使用します。
コンパイルして、これらすべてを実行してみてください。 魔法が始まろうとしている...
検証チェック
次のようなものを呼び出す場合:
var registerUser = new UserView() { Email = "user@sample.com", Password = "123456", ConfirmPassword = "1234567", AvatarPath = "/file/no-image.jpg", BirthdateDay = 1, BirthdateMonth = 12, BirthdateYear = 1987, Captcha = "1234" }; var result = controller.Register(registerUser);
第一に、暗黙の検証は実行されません。第二に、そこでセッションがあり、それを初期化しませんでした。nullであり、それがすべてです-エラー そのため、検証チェック(属性内のチェック)は別のクラスを介して配置されます。 Validator Validatorovich(/Tools/Validator.cs)と呼びましょう:
public class ValidatorException : Exception { public ValidationAttribute Attribute { get; private set; } public ValidatorException(ValidationException ex, ValidationAttribute attribute) : base(attribute.GetType().Name, ex) { Attribute = attribute; } } public class Validator { public static void ValidateObject<T>(T obj) { var type = typeof(T); var meta = type.GetCustomAttributes(false).OfType<MetadataTypeAttribute>().FirstOrDefault(); if (meta != null) { type = meta.MetadataClassType; } var typeAttributes = type.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>(); var validationContext = new ValidationContext(obj); foreach (var attribute in typeAttributes) { try { attribute.Validate(obj, validationContext); } catch (ValidationException ex) { throw new ValidatorException(ex, attribute); } } var propertyInfo = type.GetProperties(); foreach (var info in propertyInfo) { var attributes = info.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>(); foreach (var attribute in attributes) { var objPropInfo = obj.GetType().GetProperty(info.Name); try { attribute.Validate(objPropInfo.GetValue(obj, null), validationContext); } catch (ValidationException ex) { throw new ValidatorException(ex, attribute); } } } } }
ここで何が起こっているのか。 最初に、ValidationAttribute型のクラスTのすべての属性を取得します。
var typeAttributes = type.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>(); var validationContext = new ValidationContext(obj); foreach (var attribute in typeAttributes) { try { attribute.Validate(obj, validationContext); } catch (ValidationException ex) { throw new ValidatorException(ex, attribute); } }
次に、各プロパティについて同様です:
var propertyInfo = type.GetProperties(); foreach (var info in propertyInfo) { var attributes = info.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>(); foreach (var attribute in attributes) { var objPropInfo = obj.GetType().GetProperty(info.Name); try { attribute.Validate(objPropInfo.GetValue(obj, null), validationContext); } catch (ValidationException ex) { throw new ValidatorException(ex, attribute); } } }
検証が失敗すると、例外が発生し、ValidatorExceptionでラップして、例外が発生した属性を渡します。
次に、キャプチャとセッションについて説明します。 コンテキストをコントローラーに渡す必要があります(MockHttpContext):
var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>(); var httpContext = new MockHttpContext().Object; ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); controller.ControllerContext = context; controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111");
そして今、すべて一緒に:
[Test] public void Index_RegisterUserWithDifferentPassword_ExceptionCompare() { //init var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>(); var httpContext = new MockHttpContext().Object; ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); controller.ControllerContext = context; //act var registerUserView = new UserView() { Email = "user@sample.com", Password = "123456", ConfirmPassword = "1234567", AvatarPath = "/file/no-image.jpg", BirthdateDay = 1, BirthdateMonth = 12, BirthdateYear = 1987, Captcha = "1111" }; try { Validator.ValidateObject<UserView>(registerUserView); } catch (Exception ex) { Assert.IsInstanceOf<ValidatorException>(ex); Assert.IsInstanceOf<System.ComponentModel.DataAnnotations.CompareAttribute>(((ValidatorException)ex).Attribute); } }
開始すると、すべてが判明しました。 ただし、captchaはコントローラーメソッドで直接チェックされます。 特にキャプチャの場合:
[Test] public void Index_RegisterUserWithWrongCaptcha_ModelStateWithError() { //init var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>(); var httpContext = new MockHttpContext().Object; ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); controller.ControllerContext = context; controller.Session.Add(CaptchaImage.CaptchaValueKey, "2222"); //act var registerUserView = new UserView() { Email = "user@sample.com", Password = "123456", ConfirmPassword = "1234567", AvatarPath = "/file/no-image.jpg", BirthdateDay = 1, BirthdateMonth = 12, BirthdateYear = 1987, Captcha = "1111" }; var result = controller.Register(registerUserView); Assert.AreEqual(" ", controller.ModelState["Captcha"].Errors[0].ErrorMessage); }
かっこいい!
認可チェック
たとえば、管理者の下にいない場合は、承認された部分([Authorize(Roles =“ admin ")]属性でマークされたコントローラー)で-通常のユーザーが入力できないことを確認する必要があります。 これを確認する素晴らしい方法があります。 ControllerActionInvokerクラスに注目して、呼び出しのためにそれを継承しましょう(/Fake/FakeControllerActionInvoker.cs + FakeValueProvider.cs):
public class FakeValueProvider { protected Dictionary<string, object> Values { get; set; } public FakeValueProvider() { Values = new Dictionary<string, object>(); } public object this[string index] { get { if (Values.ContainsKey(index)) { return Values[index]; } return null; } set { if (Values.ContainsKey(index)) { Values[index] = value; } else { Values.Add(index, value); } } } } public class FakeControllerActionInvoker<TExpectedResult> : ControllerActionInvoker where TExpectedResult : ActionResult { protected FakeValueProvider FakeValueProvider { get; set; } public FakeControllerActionInvoker() { FakeValueProvider = new FakeValueProvider(); } public FakeControllerActionInvoker(FakeValueProvider fakeValueProvider) { FakeValueProvider = fakeValueProvider; } protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) { return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters); } protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor) { var obj = FakeValueProvider[parameterDescriptor.ParameterName]; if (obj != null) { return obj; } return parameterDescriptor.DefaultValue; } protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) { Assert.IsInstanceOf<TExpectedResult>(actionResult); } }
本質的に、それはコントローラアクションメソッドの「呼び出し側」であり、Genericクラスは予想される結果クラスです。 承認されていない場合は、HttpUnauthorizedResultになります。 テストを行いましょう(/Test/Admin/HomeControllerTest.cs):
[TestFixture] public class AdminHomeControllerTest { [Test] public void Index_NotAuthorizeGetDefaultView_RedirectToLoginPage() { var auth = DependencyResolver.Current.GetService<IAuthentication>(); auth.Login("chernikov@gmail.com", "password2", false); var httpContext = new MockHttpContext(auth).Object; var controller = DependencyResolver.Current.GetService<Areas.Admin.Controllers.HomeController>(); var route = new RouteData(); route.Values.Add("controller", "Home"); route.Values.Add("action", "Index"); route.Values.Add("area", "Admin"); ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller); controller.ControllerContext = context; var controllerActionInvoker = new FakeControllerActionInvoker<HttpUnauthorizedResult>(); var result = controllerActionInvoker.InvokeAction(controller.ControllerContext, "Index"); } }
テストを実行すると、合格します。 , admin ViewResult:
[Test] public void Index_AdminAuthorize_GetViewResult() { var auth = DependencyResolver.Current.GetService<IAuthentication>(); auth.Login("admin", "password", false); var httpContext = new MockHttpContext(auth).Object; var controller = DependencyResolver.Current.GetService<Areas.Admin.Controllers.HomeController>(); var route = new RouteData(); route.Values.Add("controller", "Home"); route.Values.Add("action", "Index"); route.Values.Add("area", "Admin"); ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller); controller.ControllerContext = context; var controllerActionInvoker = new FakeControllerActionInvoker<ViewResult>(); var result = controllerActionInvoker.InvokeAction(controller.ControllerContext, "Index"); }
. .
, . , , . . ? , , , . , Mock- , , , , ? , - ? NerdDinner .
IRepository, SqlRepository, MockRepository. SqlRepository – . . どうする? TDD?
, SqlRepository. Web.config ( ), , , , .
LessonProject.IntegrationTest Test.
Ninject, Moq NUnit:
Install-Package Ninject Install-Package Moq Install-Package NUnit
Sandbox Setup UnitTestSetupFixture (/Setup/IntegrationTestSetupFixture.cs) :
[SetUpFixture] public class IntegrationTestSetupFixture : UnitTestSetupFixture { public class FileListRestore { public string LogicalName { get; set; } public string Type { get; set; } } protected static string NameDb = "LessonProject"; protected static string TestDbName; private void CopyDb(StandardKernel kernel, out FileInfo sandboxFile, out string connectionString) { var config = kernel.Get<IConfig>(); var db = new DataContext(config.ConnectionStrings("ConnectionString")); TestDbName = string.Format("{0}_{1}", NameDb, DateTime.Now.ToString("yyyyMMdd_HHmmss")); Console.WriteLine("Create DB = " + TestDbName); sandboxFile = new FileInfo(string.Format("{0}\\{1}.bak", Sandbox, TestDbName)); var sandboxDir = new DirectoryInfo(Sandbox); //backupFile var textBackUp = string.Format(@"-- Backup the database BACKUP DATABASE [{0}] TO DISK = '{1}' WITH COPY_ONLY", NameDb, sandboxFile.FullName); db.ExecuteCommand(textBackUp); var restoreFileList = string.Format("RESTORE FILELISTONLY FROM DISK = '{0}'", sandboxFile.FullName); var fileListRestores = db.ExecuteQuery<FileListRestore>(restoreFileList).ToList(); var logicalDbName = fileListRestores.FirstOrDefault(p => p.Type == "D"); var logicalLogDbName = fileListRestores.FirstOrDefault(p => p.Type == "L"); var restoreDb = string.Format("RESTORE DATABASE [{0}] FROM DISK = '{1}' WITH FILE = 1, MOVE N'{2}' TO N'{4}\\{0}.mdf', MOVE N'{3}' TO N'{4}\\{0}.ldf', NOUNLOAD, STATS = 10", TestDbName, sandboxFile.FullName, logicalDbName.LogicalName, logicalLogDbName.LogicalName, sandboxDir.FullName); db.ExecuteCommand(restoreDb); connectionString = config.ConnectionStrings("ConnectionString").Replace(NameDb, TestDbName); } }
順番に:
var config = kernel.Get<IConfig>(); var db = new DataContext(config.ConnectionStrings("ConnectionString"));
— .
TestDbName = string.Format("{0}_{1}", NameDb, DateTime.Now.ToString("yyyyMMdd_HHmmss"));
.
//backupFile var textBackUp = string.Format(@"-- Backup the database BACKUP DATABASE [{0}] TO DISK = '{1}' WITH COPY_ONLY", NameDb, sandboxFile.FullName); db.ExecuteCommand(textBackUp);
— Sandbox.
var restoreFileList = string.Format("RESTORE FILELISTONLY FROM DISK = '{0}'", sandboxFile.FullName); var fileListRestores = db.ExecuteQuery<FileListRestore>(restoreFileList).ToList(); var logicalDbName = fileListRestores.FirstOrDefault(p => p.Type == "D"); var logicalLogDbName = fileListRestores.FirstOrDefault(p => p.Type == "L");
— , FIleListRestore.
var restoreDb = string.Format("RESTORE DATABASE [{0}] FROM DISK = '{1}' WITH FILE = 1, MOVE N'{2}' TO N'{4}\\{0}.mdf', MOVE N'{3}' TO N'{4}\\{0}.ldf', NOUNLOAD, STATS = 10", TestDbName, sandboxFile.FullName, logicalDbName.LogicalName, logicalLogDbName.LogicalName, sandboxDir.FullName); db.ExecuteCommand(restoreDb);
— (TestDbName)
connectionString = config.ConnectionStrings("ConnectionString").Replace(NameDb, TestDbName);
— connectionString.
IRepository SqlRepository:
protected override void InitRepository(StandardKernel kernel) { FileInfo sandboxFile; string connectionString; CopyDb(kernel, out sandboxFile, out connectionString); kernel.Bind<webTemplateDbDataContext>().ToMethod(c => new webTemplateDbDataContext(connectionString)); kernel.Bind<IRepository>().To<SqlRepository>().InTransientScope(); sandboxFile.Delete(); }
, sandboxFile – , connectionString – ( ). , SqlRepository, . . .
, :
private void RemoveDb() { var config = DependencyResolver.Current.GetService<IConfig>(); var db = new DataContext(config.ConnectionStrings("ConnectionString")); var textCloseConnectionTestDb = string.Format(@"ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", TestDbName); db.ExecuteCommand(textCloseConnectionTestDb); var textDropTestDb = string.Format(@"DROP DATABASE [{0}]", TestDbName); db.ExecuteCommand(textDropTestDb); }
TestDbName, ( ), .
Web.config:
xcopy $(SolutionDir)LessonProject\Web.config $(ProjectDir)Sandbox\ /y
, . , , . . – :
[TestFixture] public class DefaultUserControllerTest { [Test] public void CreateUser_CreateNormalUser_CountPlusOne() { var repository = DependencyResolver.Current.GetService<IRepository>(); var controller = DependencyResolver.Current.GetService<LessonProject.Areas.Default.Controllers.UserController>(); var countBefore = repository.Users.Count(); var httpContext = new MockHttpContext().Object; var route = new RouteData(); route.Values.Add("controller", "User"); route.Values.Add("action", "Register"); route.Values.Add("area", "Default"); ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller); controller.ControllerContext = context; controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111"); var registerUserView = new UserView() { ID = 0, Email = "rollinx@gmail.com", Password = "123456", ConfirmPassword = "123456", Captcha = "1111", BirthdateDay = 13, BirthdateMonth = 9, BirthdateYear = 1970 }; Validator.ValidateObject<UserView>(registerUserView); controller.Register(registerUserView); var countAfter = repository.Users.Count(); Assert.AreEqual(countBefore + 1, countAfter); } }
, email.
実行して確認してください。 . ! , . - – , – . , , , MailNotify . :
/LessonProject/Tools/Mail/IMailSender.cs:
public interface IMailSender { void SendMail(string email, string subject, string body, MailAddress mailAddress = null); }
/LessonProject/Tools/Mail/MailSender.cs:
public class MailSender : IMailSender { [Inject] public IConfig Config { get; set; } private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); public void SendMail(string email, string subject, string body, MailAddress mailAddress = null) { try { if (Config.EnableMail) { if (mailAddress == null) { mailAddress = new MailAddress(Config.MailSetting.SmtpReply, Config.MailSetting.SmtpUser); } MailMessage message = new MailMessage( mailAddress, new MailAddress(email)) { Subject = subject, BodyEncoding = Encoding.UTF8, Body = body, IsBodyHtml = true, SubjectEncoding = Encoding.UTF8 }; SmtpClient client = new SmtpClient { Host = Config.MailSetting.SmtpServer, Port = Config.MailSetting.SmtpPort, UseDefaultCredentials = false, EnableSsl = Config.MailSetting.EnableSsl, Credentials = new NetworkCredential(Config.MailSetting.SmtpUserName, Config.MailSetting.SmtpPassword), DeliveryMethod = SmtpDeliveryMethod.Network }; client.Send(message); } else { logger.Debug("Email : {0} {1} \t Subject: {2} {3} Body: {4}", email, Environment.NewLine, subject, Environment.NewLine, body); } } catch (Exception ex) { logger.Error("Mail send exception", ex.Message); } } }
/LessonProject/Tools/Mail/NotifyMail.cs:
public static class NotifyMail { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static IConfig _config; public static IConfig Config { get { if (_config == null) { _config = (DependencyResolver.Current).GetService<IConfig>(); } return _config; } } private static IMailSender _mailSender; public static IMailSender MailSender { get { if (_mailSender == null) { _mailSender = (DependencyResolver.Current).GetService<IMailSender>(); } return _mailSender; } } public static void SendNotify(string templateName, string email, Func<string, string> subject, Func<string, string> body) { var template = Config.MailTemplates.FirstOrDefault(p => string.Compare(p.Name, templateName, true) == 0); if (template == null) { logger.Error("Can't find template (" + templateName + ")"); } else { MailSender.SendMail(email, subject.Invoke(template.Subject), body.Invoke(template.Template)); } } }
/LessonProject/App_Start/NinjectWebCommon.cs:
private static void RegisterServices(IKernel kernel) {… kernel.Bind<IMailSender>().To<MailSender>(); }
LessonProject.UnitTest MockMailSender (/Mock/Mail/MockMailSender.cs):
public class MockMailSender : Mock<IMailSender> { public MockMailSender(MockBehavior mockBehavior = MockBehavior.Strict) : base(mockBehavior) { this.Setup(p => p.SendMail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MailAddress>())) .Callback((string email, string subject, string body, MailAddress address) => Console.WriteLine(String.Format("Send mock email to: {0}, subject {1}", email, subject))); } }
UnitTestSetupFixture.cs (/LessonProject.UnitTest/Setup/UnitTestSetupFixture.cs):
protected virtual IKernel InitKernel() { … kernel.Bind<MockMailSender>().To<MockMailSender>(); kernel.Bind<IMailSender>().ToMethod(p => kernel.Get<MockMailSender>().Object); return kernel; }
, , .
=============== =====START===== =============== Create DB = LessonProject_20130314_104218 Send mock email to: chernikov@googlemail.com, subject =============== =====BYE!====== ===============
, . () GenerateData Test, , . . – , . , ( , , ).
« », , , .
100 :
[Test] public void CreateUser_Create100Users_NoAssert() { var repository = DependencyResolver.Current.GetService<IRepository>(); var controller = DependencyResolver.Current.GetService<LessonProject.Areas.Default.Controllers.UserController>(); var httpContext = new MockHttpContext().Object; var route = new RouteData(); route.Values.Add("controller", "User"); route.Values.Add("action", "Register"); route.Values.Add("area", "Default"); ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller); controller.ControllerContext = context; controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111"); var rand = new Random((int)DateTime.Now.Ticks); for (int i = 0; i < 100; i++) { var registerUserView = new UserView() { ID = 0, Email = Email.GetRandom(Name.GetRandom(), Surname.GetRandom()), Password = "123456", ConfirmPassword = "123456", Captcha = "1111", BirthdateDay = rand.Next(28) + 1, BirthdateMonth = rand.Next(12) + 1, BirthdateYear = 1970 + rand.Next(20) }; controller.Register(registerUserView); } }
IntegrationTestSetupFixture.cs (/Setup/IntegrationTestSetupFixture.cs):
protected static bool removeDbAfter = false;
Web.config :
<add name="ConnectionString" connectionString="Data Source=SATURN-PC;Initial Catalog=LessonProject_20130314_111020;Integrated Security=True;Pooling=False" providerName="System.Data.SqlClient" />
:
まとめ
:
- TDD
- NUnit
- Mock
- Unit-
- Integration-,
– , ( ). , , , TDD . – , , …
, , , . JQuery 2011 qUnit, .
すべてのソースはhttps://bitbucket.org/chernikov/lessonsにあります - パラメータなし