10

私はMVC、ユニットテスト、モック、TDDを初めて使用します。私は可能な限りベストプラクティスに従うようにしています。

コントローラの単体テストを作成しましたが、正しいビューが返されるかどうかのテストに問題があります。ViewResult.ViewNameを使用する場合、コントローラーでビュー名を指定しないと、テストは常に失敗します。コントローラでViewNameを指定すると、ビューが存在しない場合でも、テストは常に合格します。

Response.Statusコードのテストも試みましたが、これは常に200を返します(MVC3ユニットテストの応答コードに対するDarin Dimitrovの回答から取得したコード)。私が目指しているのは、新しいビューを作成するときの古典的な赤、緑のリファクタリングであり、ライブに移行するときの404およびSystem.InvalidOperationExceptionエラーを回避することです。これは可能ですか?

以下のコード。

public class BugStatusController : Controller
{
    public ActionResult Index(){
        return View(); // Test always fails as view name isn’t specified even if the correct view is returned.
    }

    public ActionResult Create(){
        return View("Create"); // Test always passes as view name is specified even if the view doesn’t exist.
    }
}

[TestFixture]
public class BugStatusTests
{    
    private ViewResult GetViewResult(Controller controller, string controllerMethodName){
        Type type = controller.GetType();
        ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);

        object instance = constructor.Invoke(new object[] {});
        MethodInfo[] methods = type.GetMethods();

        MethodInfo methodInfo = (from method in methods
                                where method.Name == controllerMethodName
                                                    && method.GetParameters().Count() == 0
                                select method).FirstOrDefault();

        Assert.IsNotNull(methodInfo, "The controller {0} has no method called {1}", type.Name, controllerMethodName);

        ViewResult result = methodInfo.Invoke(instance, new object[] {}) as ViewResult;

        Assert.IsNotNull(result, "The ViewResult is null, controller: {0}, view: {1}", type.Name, controllerMethodName);

        return result;
    }

    [Test]
    [TestCase("Index", "Index")]
    [TestCase("Create", "Create")]
    public void TestExpectedViewIsReturned(string expectedViewName, string controllerMethodName){
        ViewResult result = GetViewResult(new BugStatusController(), controllerMethodName);

        Assert.AreEqual(expectedViewName, result.ViewName, "Unexpected view returned, controller: {0}, view: {1}", CONTROLLER_NAME, expectedViewName);
    }

    [Test]
    [TestCase("Index", "Index")]
    [TestCase("Create", "Create")]
    public void TestExpectedStatusCodeIsReturned(string expectedViewName, string controllerMethodName)
    {
        var controller = new BugStatusController();
        var request = new HttpRequest("", "http://localhost:58687/", "");
        var response = new HttpResponse(TextWriter.Null);
        var httpContext = new HttpContextWrapper(new HttpContext(request, response));
        controller.ControllerContext = new ControllerContext(httpContext, new RouteData(), controller);

        ActionResult result = GetViewResult(controller, controllerMethodName);

        Assert.AreEqual(200, response.StatusCode, "Failed to load " + expectedViewName + " Error: "  + response.StatusDescription);
    }
}
4

1 に答える 1

26

私はMVC、ユニットテスト、モック、TDDを初めて使用します。私は可能な限りベストプラクティスに従うようにしています。

ますます多くの開発者がコードの単体テストを書き始めていることを嬉しく思います。それで、あなたが正しい道を進んでいることを祝福します。

コントローラでビュー名を指定しない場合。コントローラでViewNameを指定すると、ビューが存在しない場合でも、テストは常に合格します。

メソッドでビュー名を指定しないView場合、これはMVCエンジンにデフォルトのビューをレンダリングするように指示します。たとえば、

public ActionResult Index() { return View(); }

上記のコードは空のビュー名を返します。これは、レンダリングされたビューがアクションの名前になることを意味します。この場合は、インデックスになります。

したがって、アクションがデフォルトのビューを返すことをテストする場合は、返されたビュー名が空であることをテストする必要があります。

ビューが存在しない場合でも、ビュー名が指定されているため、テストは常に合格です。

ここで何が起こっているのかを説明するために、最初にアクションフィルターがどのように機能するかを説明します。

フィルタには基本的に4種類あります

  • 例外フィルター
  • 承認フィルター
  • アクションフィルター
  • 結果フィルター

アクションと結果のフィルターに集中します

アクションフィルターはIActionFilterインターフェースで定義されます

public interface IActionFilter
{
    // Summary:
    //     Called after the action method executes.
    //
    void OnActionExecuted(ActionExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action method executes.
    //
    void OnActionExecuting(ActionExecutingContext filterContext);
}

結果フィルターはIResultFilterインターフェースで定義されます

public interface IResultFilter
{
    // Summary:
    //     Called after an action result executes.
    //
    void OnResultExecuted(ResultExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action result executes.
    //
    void OnResultExecuting(ResultExecutingContext filterContext);
}

コントローラのアクションが実行されると、次のフィルタがこの特定の順序で実行されます。

IActionFilter.OnActionExecuting
IActionFilter.OnActionExecuted
IResultFilter.OnResultExecuting
IResultFilter.OnResultExecuted

アクションが実行されると、別のコンポーネントがアクションから返されたものを処理しActionResult、正しいHTMLをレンダリングしてクライアントに送り返します。これは、結果が処理されるときです。

この関心の分離は、コントローラーのアクションを単体テストできるようにするための美しさと鍵です。そうでない場合、それらが結合されていると、アクションの結果を単独で単体テストすることはできません。

これで、アクションが実行された後(結果が処理されているとき)にRazorViewEngineビューを見つけようとします。そのため、物理ビューが存在しない場合でもテストはtrueを返します。これは予想される動作であり、コントローラーのアクションを個別にテストする必要があることを忘れないでください。単体テストで期待されるビューがレンダリングされると断言する限り、単体テストは完了です。

物理ビューが存在することを主張したい場合は、いくつかの特定の統合テストについて話します:機能テストまたはユーザー受け入れテスト-これらの種類のテストでは、ブラウザーを使用してアプリケーションをインスタンス化する必要がありますが、単体テストではありません

これで、単体テストを手動で記述しても問題ありません(単体テストの世界に入る場合、これは素晴らしい演習です)が、単体の記述に役立ついくつかのMVCテストフレームワークをお勧めします。本当に速いテスト

これらのフレームワークに関するいくつかの個人的なコメント

私の経験によると、MVCContribはFluentMVC Testingよりも多くの機能を備えていますが、MVC 4を使用しているため、Visual Studio 2012で機能させることができなかったため、両方を組み合わせて使用​​しています(これは汚いです)より良いアプローチが見つかるまでの回避策)

これが私がすることです:

var testControllerBuilder = new TestControllerBuilder(); // this is from MVC Contrib
var controller = new MoviesController(
    this.GetMock<IMovieQueryManager>().Object);

testControllerBuilder.InitializeController(controller); // this allows me to use the Session, Request and Response objects as mock objects, again this is provided by the MVC Contrib framework

// I should be able to call something like this but this is not working due to some problems with DLL versions (hell DLL's) between MVC Controb, Moq and MVC itself
// testControllerBuilder.CreateController<MoviesController>();

controller.WithCallTo(x => x.Index(string.Empty)).ShouldRenderDefaultView(); // this is using Fluent MVC Testing

// again instead of the above line I could use the MVC Contrib if it were working....
// var res = sut.Index(string.Empty);
// res.AssertViewRendered().ForView("Index");

これがお役に立てば幸いです=)ハッピーコーディング!

于 2012-11-08T23:41:42.293 に答える