ASP.Netで0から超簡易掲示板を作る

ASP.Netで完全に0から超簡易の掲示板を作りました。

あくまでASP.Netがどんなもんか見る為のものだったのでいろいろと雑です。

(細かいエラー処理やらセキュリティ回りはノータッチです。)

githubに上げていますのでそちらも確認してください。

また、この記事を見るにあたってMVC2と単体テストの事前知識があると読みやすいと思われます。

カラムの構成を変更するMigrationもさわってみました。

EntityFrameworkのMigrationを試してみる
以前ASP.Netで超簡単な掲示板を作りました。 これのDBの処理はEntityFrameworkと呼ばれるO/Rマッパーを利用しています。 ただ、作っていて不便な点が出てきます。それが、テーブル情報の変更時です...

※超長いので注意。

開発環境

前のページにも書きましたがVisual Studio Communityをインストールすれば終わります。

インストール時はカスタマイズで「Web Developer Tools」のチェックを忘れないようにしてください。

以下の記事はすべてVisual Studio Community 2015(英語)で行ってます。

適宜、日本語に置き換えたりで補完してください…

ソリューションを作る

まずはVS用のソリューションを作ります。

File → New Project

でプロジェクト作成ウィザードを開きます。

Templates → VisualC# → Web → ASP.NET Web Application

を選択し名前を適当につけます。

イメージ033

次にASP.Netのテンプレートの選択がありますので、Emptyを選択し、下のMVCAdd unit testにチェックを入れます。

Hosts in the cloudのチェックは外します。

イメージ034

 

次に必要なライブラリを追加します。

まずTest側のプロジェクトにWeb系の参照がないので追加します。

Solution ExplorerのTestプロジェクトのReferenceを右クリックし、Add Referenceを選択します。

BrowseからTestではない方のプロジェクトのbinフォルダ以下にあるdllを全部追加します。

イメージ038

※もちろんNuGetでテストに必要な参照を追加しても大丈夫です。

次に

Tools → NuGet Package Manger → Manage NuGet Packages for Solution

を選択しNuGetの画面を開きます。

イメージ035

BrowseのSearchに「EntityFramework」を入力しEntityFrameworkをインストールします。

イメージ036

次に同様に「Moq」を検索し、コチラはTestだけにインストールします。

イメージ037

 

ついでにデフォルトで生成されているUnitTest1.csもいらないので削除しておきます。

これで初期設定は終わりです。

Hello World.してみる

まず試しにHello Worldしてみます。

Hello Worldする為に2つほどファイルを追加します。

Solution ExplorerでControllersフォルダを右クリックしてAdd → Controllerを選択します。

MVC5 Controller – Emptyを選び名前はHomeControllerとして決定します。

※最後に必ずControllerがつかなければいけません。末尾にControllerがついてるものを目印にしてシステムが動いているようです。

作ったコントローラは特にいじらずに次のファイルを追加します。

Solution ExplorerでViewsフォルダの下のHomeフォルダを右クリックしてAdd → MVC5 View Page(Razor)を選択します。

名前はIndexとします。

htmlっぽいファイルが出来るのでdivの中にHello Worldを記述します。これで完成です。

F5を押してデバッグを開始してみます。

イメージ039

イメージ040

ざっくり仕組み

ASP.NetではMVC2というデザインパターンで作成します。

これはページをModelViewControllerという3つの要素に分解して実装していく手法です。

  • Model
    • DBへのデータ追加や検索等のビジネスロジックを実装します。
  • View
    • 実際にページに表示される内容(≒HTML)を記述します。
  • Controller
    • ユーザーからの入力を受け取って適切なModelやViewを選択します。

という感じです。

ASP.Netではこれを実装する為にいくらか制限を設けているようです。

  • それぞれModelsViewsControllersという名前のフォルダにコードを入れる
  • Controllerのクラス名の末尾には必ずControllerという文字をつける(前述のとおり)
  • Viewはコントローラ名のフォルダ以下に、そのコントローラに関連するものを入れる

もっと深い話は適宜ググってください。

作っていくもの

では、これから作っていくものを書きます。

超簡易の掲示板です。

機能は、「掲示板の追加・掲示板の一覧・掲示板への返信の投稿・掲示板の内容の確認」のみで行きます。

フローとしては

アクティビティ図0

こんな感じで行きます。

レイアウトを用意しておく

先ほどViewを用意したら色々タグが追加された状態でした。

ただ、あれだと全ページで共通したデザインにするのが面倒なので、共通部をくくりだしたレイアウトファイルというのを作成します。

 

まずViewsフォルダの下にSharedフォルダを作成します。

作成したSharedフォルダを右クリックしてAdd → MVC5 Layout Page(Razor)を選択します。

名前は「_」から始まっていればなんでもいいですが、とりあえず_DefaultLayoutにしました。

※「_」から始まるファイルはブラウザから見ることが絶対に出来なくなりますので、こういうブラウザからアクセスさせたくないレイアウト等は_をつけます。

※「Shared」フォルダは全部のViewで共有できるコードや情報を格納します。

 

出来たレイアウトファイルは文字コードの指定が無いので追記します。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

最初に作ったIndexビューもレイアウトを反映したものにしたいので一度削除します。

HomeフォルダのIndexを削除して、改めて右クリック → Add → MVC5 View Page With Layout(Razor) を選択します。名前は先ほど同様Indexとします。

レイアウトファイルの指定を促されますので、上記で作った_DefaultLayoutを指定します。

ファイルが出来たらタイトルの指定とHello Worldを追記しておきます。

@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = "とっぷぺーじ";
}

Hello World

@{}より下の部分が@RenderBody()の部分に挿入されます。

 

ブラウザをF5して確認してみましょう。

※ビューに関しては編集してもビルドしなおす必要はありませんのでF5で更新されます。コントローラとモデルを変更した場合に関してはビルドしなおす必要がありますので、一度デバッグを終了する必要があります。

トップページ作る

トップページ作ります。

と言っても上のアクティビティ図の通り掲示板の一覧ページと追加ページへのリンクのみです。

まずテスト用意しておく

HomeControllerのIndexメソッドのテストを用意しておきます。

TestプロジェクトにControllersフォルダを追加します。

更にフォルダ右クリックしてAdd → New Itemを選択し、TestのBasic Unit Testを選択します。名前はHomeControllerTestとしてファイルを生成します。

TestMethod1というのが最初に用意されていますので、これのメソッド名をIndexに変更しこんな感じに。

イメージ044

赤波線が表示されエラーが出る場合は、その箇所にマウスカーソルを持ってくると豆電球が出て解決方法を提示してくれます。

今回であればusingが不足しているので、追加してください。

イメージ043

ひとまずページ表示されるだけなので、Indexメソッドが正常終了して、返り値がnullでないかだけ確認しています。

Test ExplorerからRunして緑(成功)であることを確認しておきます。

※Test ExplorerはメニューのTest → Windows → Test Explorerで表示されます。

※Test Explorerに何も表示されない場合はCtrl + bでソリューションをビルドしてください。

イメージ045

HTML用意する

@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = "とっぷぺーじ";
}

<div>
    @Html.ActionLink( "掲示板一覧", "", "Board" )
</div>
<div>
    @Html.ActionLink( "掲示板追加", "Create", "Board" )
</div>

@Html.ActionLinkメソッドはコントローラ名とメソッド名を指定すると適切な<a>タグに置き換えてくれます。

まだ、Boardコントローラは作っていませんが、こんな感じで行きます。

用意出来たら、ブラウザでちゃんとアンカーが用意されているか確認しておきましょう。

掲示板の追加作る

次は掲示板の追加処理を作ります。

まずHomeの時と同様にBoardコントローラを作ります。

次にCreateメソッドを作ります。

using System.Web.Mvc;

namespace TestBoard.Controllers
{
    public class BoardController : Controller
    {
        // GET: Board
        public ActionResult Index()
        {
            return View();
        }

        // GET: Board/Create
        public ActionResult Create()
        {
            return View();
        }
    }
}

テスト用意する

BoardController用のテストも用意しておきます。

Homeの時と同様にBoardControllerTestを作り、IndexとCreateメソッドのテストを用意します。

※Test Explorerでのテストの名前が紛らわしい場合はメソッド名を分かりやすいもの(Home_Index、Board_Indexとか)に変えても大丈夫です。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestBoard.Controllers;
using System.Web.Mvc;

namespace TestBoard.Tests.Controllers
{
    [TestClass]
    public class BoardControllerTest
    {
        [TestMethod]
        public void Index()
        {
            var controller = new BoardController();
            var result = controller.Index() as ViewResult;
            Assert.IsNotNull( result );
        }

        [TestMethod]
        public void Create()
        {
            var controller = new BoardController();
            var result = controller.Create() as ViewResult;
            Assert.IsNotNull( result );
        }
    }
}

この2つのテストもRunして緑であることを確認しておきます。

投稿ページの入力データを受け取る関数を追加

投稿ページのフォームから送られてきたデータを受け取る関数をBoardコントローラに追加します。

        // POST: Board/Create
        [HttpPost]
        public ActionResult Create(BoardCreateModel data)
        {
            return View();
        }

BoardCreateModelが無いのでこれも用意します。

Modelsフォルダを右クリック → Add → New Item -> Classを選択します。

名前をBoardCreateModelにしておきます。

namespace TestBoard.Models
{
    public class BoardCreateModel
    {
        public string Title { get; set; }
        public string Text { get; set; }
    }
}

中身は題と本文を持つだけのものです。

テスト更新

テストを更新して、先ほど作ったCreateメソッドにも対応させます。

        [TestMethod]
        public void PostCreate()
        {
            var model = new BoardCreateModel {
                Title = "題名",
                Text = "本文"
            };

            var controller = new BoardController();
            var result = controller.Create(model) as ViewResult;
            Assert.IsNotNull( result );
        }

 

DBへアクセスするクラスを用意する

掲示板の内容はDBに保存する事になりますので、DBとやり取りするクラスを用意します。

Modelsフォルダを右クリック → Add → New Item → Class を選択します。

名前はBoardDbContextにしておきます。

次に掲示板の情報を格納するクラスを用意します。

イメージ046

大体こんな感じになりますので、そのままクラスに

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;

namespace TestBoard.Models
{
    public class BoardEntity
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Title { get; set; }

        [Required]
        public string Text { get; set; }

        public virtual ICollection<BoardPostEntity> Posts { get; set; }
    }

    public class BoardPostEntity
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Text { get; set; }
    }

    public class BoardDbContext
    {
    }
}

KeyとかRequiredなんかの属性がついていますが、これはそのままDBのキーであったりNot Nullであったりを表します。

リレーションシップはICollection<BoardPostEntity>をメンバに入れることで自動で解決してくれます。

肝心のBoardDbContextを実装します。

    public class BoardDbContext : DbContext
    {
        public BoardDbContext()
            : base( "DefaultConnection" )
        {
        }

        public virtual DbSet<BoardEntity> Boards { get; set; }
    }

その継承しているDbContextがすごいやつでメンバ変数の内容から自動でDBのテーブル用意してくれたりSELECTしてくれたりと至れりつくせりです。

 

BoardControllerはこのBoardDbContextを所持してDBとアクセスできるようにします。

using System.Web.Mvc;
using TestBoard.Models;

namespace TestBoard.Controllers
{
    public class BoardController : Controller
    {
        private BoardDbContext db_;

        public BoardController() : this( null )
        {
        }

        public BoardController( BoardDbContext db )
        {
            db_ = db ?? new BoardDbContext();
        }

        // GET: Board
        public ActionResult Index()
        {
            return View();
        }

        // GET: Board/Create
        public ActionResult Create()
        {
            return View();
        }

        // POST: Board/Create
        [HttpPost]
        public ActionResult Create(BoardCreateModel data)
        {
            return View();
        }
    }
}

メンバを追加しました。

テスト更新

テストのPostCreateを更新します。

Create(BoardCreateModel)メソッドではフォームの入力値(data)を受け取ってその内容でDBのBoardEntityをインサートする処理なのでそれに合わせてテストを実装しておきます。

 

テストコードでDBを直接更新するわけにはいかないのでモックに代わりにさせます。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestBoard.Controllers;
using System.Web.Mvc;
using TestBoard.Models;
using System.Data.Entity;
using Moq;

namespace TestBoard.Tests.Controllers
{
    [TestClass]
    public class BoardControllerTest
    {
// 略
        [TestMethod]
        public void PostCreate()
        {
            // モック用意
            var mockset = new Mock<DbSet<BoardEntity>>();
            var mockcontext = new Mock<BoardDbContext>();

            mockcontext.Setup( m => m.Boards ).Returns( mockset.Object );

            var model = new BoardCreateModel {
                Title = "題名",
                Text = "本文"
            };

            var controller = new BoardController(mockcontext.Object);
            var result = controller.Create(model) as ViewResult;
            Assert.IsNotNull( result );

            // Addが呼ばれたかチェック
            mockset.Verify( m => m.Add( It.Is<BoardEntity>( o => o.Title == model.Title && o.Text == model.Text ) ), Times.Once );

            // SaveChangesがよばれたかチェック
            mockcontext.Verify( m => m.SaveChanges(), Times.Once );
        }
    }
}

モックはメソッドが何回呼ばれたか等が記録されます。

Createメソッドでは掲示板の追加にAddが1回、追加分の確定にSaveChangesが1回呼ばれるはずなのでそれをチェックします。

テストをRunしてみてください。未実装なのでもちろん赤(失敗)になるはずです。

DBへの書き込み処理を実装

        // POST: Board/Create
        [HttpPost]
        public ActionResult Create(BoardCreateModel data)
        {
            db_.Boards.Add( new BoardEntity {
                Title = data.Title,
                Text = data.Text
            } );
            db_.SaveChanges();
            return View();
        }

AddしてSaveするだけです。

実装したらテストをRunして緑になることを確認します。

投稿ページ作る

コントローラはなんとなくできたので次は投稿ページのビューを用意します。

ViewsのBoardのところにView With Layoutでビューを「Create」という名前で追加します。

中身は

@model TestBoard.Models.BoardCreateModel
@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = "掲示板の追加";
}

@using (Html.BeginForm("Create", "Board", FormMethod.Post))
{
    <p>タイトル:
        @Html.TextBoxFor( m => m.Title )
    </p>
    <p>本文:</p>
    @Html.TextAreaFor( m => m.Text )
    <div>
        <input type="submit" value="作成" />
    </div>
}

@modelを指定することでこのビューとモデルを関連づけることが出来ます。

関連づいてるとインテリセンスが働くのでコードの記述が楽になります。

@using (Html.BeginForm()){}を使う事で<form>タグを挿入してくれます。

TextBoxForやTextAreaForは関連付けられたモデルを元にテキストボックス等のコンポーネントを配置してくれます。

 

これでブラウザから投稿してみましょう。

通信終わって同じ画面になってると思いますが、投稿は成功しています。

DBの中身を直接確認してみます。

Solution ExplorerのApp_Dataフォルダを右クリックしてAdd → Existing Itemを選択し、App_Dataの下のDefaultConnection.mdfを選択します。

Solution Explorerに追加出来たらWクリックします。Sever Explorerが表示されますのでテーブルを選択し、Show Table Dataで中身が確認できます。

イメージ047

作成された掲示板の一覧

次は前項で作った掲示板を一覧するページを作ります。

現在のBoard#Indexを投稿された掲示板を一覧するものに作り替えます。

モデルクラス用意

モデルクラス用意します。

using System.Collections.Generic;

namespace TestBoard.Models
{
    public class BoardListModel
    {
        public List<BoardEntity> Boards { get; private set; }

        public BoardListModel()
        {
        }
    }
}

ひとまずクラスを用意しただけです。

とりあえずテスト更新する

Indexのテストを更新してDBから掲示板情報を取得するようにします。

PostCreateと同様にモックを用意します。

モック → コントローラの処理 → ビューに渡されているモデルの中身を検証という流れです。

        [TestMethod]
        public void Index()
        {
            // DBのモックを用意する
            var mockset = new Mock<DbSet<BoardEntity>>();
            var mockcontext = new Mock<BoardDbContext>();

            var originalData = new List<BoardEntity> {
                new BoardEntity { Id = 1, Title = "A", Text = "a" },
                new BoardEntity { Id = 2, Title = "B", Text = "b" },
                new BoardEntity { Id = 3, Title = "C", Text = "c" },
            };
            var data = originalData.AsQueryable();

            // 各メソッドの返り値をモックに差し替える
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Provider ).Returns( data.Provider );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Expression ).Returns( data.Expression );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.ElementType ).Returns( data.ElementType );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.GetEnumerator() ).Returns( data.GetEnumerator() );

            mockcontext.Setup( m => m.Boards ).Returns( mockset.Object );

            var controller = new BoardController( mockcontext.Object );
            ViewResult result = controller.Index() as ViewResult;

            //  モデルのデータがちゃんとDBのデータを取得出来ているか検証
            var model = result.Model as BoardListModel;
            Assert.AreSame( originalData[0], model.Boards[0] );
            Assert.AreSame( originalData[1], model.Boards[1] );
            Assert.AreSame( originalData[2], model.Boards[2] );

            Assert.IsNotNull( result );
        }

こんな感じです。ちゃんと赤になることを確認します。

失敗したテストの解決

まず、modelがnullというエラーが出ているのでこれを解決します。

        // GET: Board
        public ActionResult Index()
        {
            var model = new BoardListModel();
            return View( model );
        }

さて次はBoardsがnullというエラーが出ているのでこれを解決します。

        public BoardListModel()
        {
            Boards = new List<BoardEntity>();
        }

Boards[0]が配列外というエラーが出るのでこれを解決します。

        public BoardListModel()
        {
            Boards = new List<BoardEntity>( new BoardEntity[3] );
        }

もちろんDB見てないのでAreSameでエラーが出ますので、DBから読み込むようにします。

using System.Collections.Generic;
using System.Linq;

namespace TestBoard.Models
{
    public class BoardListModel
    {
        public List<BoardEntity> Boards { get; private set; }

        public BoardListModel(BoardDbContext db)
        {
            Boards = db.Boards.ToList();
        }
    }
}
        // GET: Board
        public ActionResult Index()
        {
            var model = new BoardListModel( db_ );
            return View( model );
        }

はい、無事テストとおり緑になりました。

一覧画面を作成

DBから読み出せてそうなので、実際に画面を作って本当に出来ているか確認します。

View\Boardの下にIndexビューを用意してこんな感じに

@model TestBoard.Models.BoardListModel
@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = "掲示板の一覧";
}

@foreach ( var board in Model.Boards )
{
    <div><p>@board.Title</p></div>
}

掲示板の詳細

投稿された掲示板の中身と投稿されたレスの画面を作ります。

とりあえずテスト追加

新たにBoardにShowメソッドを追加する前提でテストを追加します。

引数に掲示板のIDを指定したら結果が返ってくるというもので行きます。

        [TestMethod]
        public void Show()
        {
            var controller = new BoardController();
            ViewResult result = controller.Show(1) as ViewResult;
            Assert.IsNotNull( result );
        }

もちろんコンパイルが通らないのでコントローラにメソッド足します。

        // GET: Board/Show/{ID}
        public ActionResult Show( int id )
        {
            return View();
        }

さて、これを一覧の時と同様に作っていきます。

ざっくり飛ばし気味でまずテストから

        [TestMethod]
        public void Show()
        {
            // DBのモックを用意する
            var mockset = new Mock<DbSet<BoardEntity>>();
            var mockcontext = new Mock<BoardDbContext>();

            // 掲示板の情報
            var postOriginalData = new List<BoardPostEntity> {
                new BoardPostEntity { Text = "投稿1" },
                new BoardPostEntity { Text = "投稿2" }
            };

            // レスの情報
            var originalData = new List<BoardEntity> {
                new BoardEntity { Id = 1, Title = "A", Text = "a", Posts = postOriginalData },
            };
            var data = originalData.AsQueryable();

            // 各メソッドの返り値をモックに差し替える
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Provider ).Returns( data.Provider );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Expression ).Returns( data.Expression );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.ElementType ).Returns( data.ElementType );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.GetEnumerator() ).Returns( data.GetEnumerator() );

            mockcontext.Setup( m => m.Boards ).Returns( mockset.Object );

            var controller = new BoardController( mockcontext.Object );
            ViewResult result = controller.Show( 1 ) as ViewResult;

            //  モデルのデータがちゃんとDBのデータを取得出来ているか検証
            var model = result.Model as BoardEntity;
            Assert.AreSame( originalData[0], model );
            Assert.AreSame( postOriginalData[0], model.Posts.ToArray()[0] );
            Assert.AreSame( postOriginalData[1], model.Posts.ToArray()[1] );

            Assert.IsNotNull( result );
        }

こんな感じです。赤になることを確認したらコントローラを実装します。

        // GET: Board/Show/{ID}
        public ActionResult Show( int id )
        {
            var board = (from o in db_.Boards where o.Id == id select o).DefaultIfEmpty( null ).Single();
            return View( board );
        }

from~でコンパイルエラーが出る場合はusing Linq;を追加してください。

これで緑になります。

ビュー作る

@model TestBoard.Models.BoardEntity
@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = Model.Title;
}

<div>
    <p>@Model.Title</p>
    <p>@Model.Text</p>
</div>

@if ( Model.Posts != null )
{
    <div>
        @foreach ( var post in Model.Posts )
        {
            <div>
                @post.Text
            </div>
        }
    </div>
}

モデルの内容をそのまま出すだけなので特に何もありません。

確認するときは、まだリンクを用意していないので「http://localhost:ポート/Board/Show/1」のような感じで直でアクセスしましょう。

掲示板一覧から詳細へリンクを作る

@model TestBoard.Models.BoardListModel
@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = "掲示板の一覧";
}

@foreach ( var board in Model.Boards )
{
    <div>
        <p>
            @Html.ActionLink( @board.Title, "Show", new { id = board.Id } )
        </p>
    </div>
}

これもActionLinkで<a>タグを生成するようにしたくらいです。

new { id = board.Id }と記述することで、Showメソッドの引数idに掲示板のIDを渡すことが出来ます。

レスの投稿処理

レスを投稿できるようにします。

やっぱりテストから

何が無くてもとりあえずテストを追加します。

今回はPostResponseというメソッドがコントローラに追加される前提で実装します。

namespace TestBoard.Models
{
    public class BoardPostModel
    {
        public string Text { get; set; }
    }
}

まず投稿用データの受け渡しモデルです。

        [TestMethod]
        public void PostResponse()
        {
            // DBのモックを用意する
            var mockposts = new Mock<ICollection<BoardPostEntity>>();
            var mockset = new Mock<DbSet<BoardEntity>>();
            var mockcontext = new Mock<BoardDbContext>();

            var originalData = new List<BoardEntity> {
                new BoardEntity { Id = 1, Title = "A", Text = "a", Posts = mockposts.Object },
            };
            var data = originalData.AsQueryable();

            // 各メソッドの返り値をモックに差し替える
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Provider ).Returns( data.Provider );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.Expression ).Returns( data.Expression );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.ElementType ).Returns( data.ElementType );
            mockset.As<IQueryable<BoardEntity>>().Setup( m => m.GetEnumerator() ).Returns( data.GetEnumerator() );

            mockcontext.Setup( m => m.Boards ).Returns( mockset.Object );

            var postData = new BoardPostModel { Text = "投稿内容" };

            var controller = new BoardController(mockcontext.Object);
            var result = controller.PostResponse(1, postData ) as RedirectResult;

            //  データの追加がちゃんとされているかチェック
            mockposts.Verify( m => m.Add( It.Is<BoardPostEntity>( o => o.Text == postData.Text ) ), Times.Once );
            mockcontext.Verify( m => m.SaveChanges(), Times.Once );

        }

テストです。

コントローラも実装します。

        // POST: Board/PostResponse/{id}
        [HttpPost]
        public ActionResult PostResponse( int id, BoardPostModel data )
        {
            var board = (from o in db_.Boards where o.Id == id select o).DefaultIfEmpty( null ).Single();
            if ( board != null ) {
                board.Posts.Add( new BoardPostEntity {
                    Text = data.Text
                } );
                db_.SaveChanges();
            }
            return Redirect("/Board/Show/" + id);
        }

今までと違って返り値をRedirectにしました。

詳細ビューに投稿フォームを追加

@model TestBoard.Models.BoardEntity
@{
    Layout = "~/Views/Shared/_DefaultLayout.cshtml";
    ViewBag.Title = Model.Title;
}

<div>
    <p>@Model.Title</p>
    <p>@Model.Text</p>
</div>

@if ( Model.Posts != null )
{
    <div>
        @foreach ( var post in Model.Posts )
        {
            <div>
                @post.Text
            </div>
        }
    </div>
}

<div>
    @using (Html.BeginForm("PostResponse", "Board", new { Id = Model.Id }, FormMethod.Post ) )
    {
        <div>
            @Html.TextArea( "Text", "" )
        </div>
        <input type="submit" value="投稿" />
    }
</div>

下にフォームを足しました。

以前と違いTextAreaForを使っていません。というのも、このビューに関連づいているモデル(BoardEntity)とデータ受け渡し用のモデル(BoardPostModel)が違う為使えません。

BoardPostModelのメンバを文字列で指定することで、インテリセンスは働かないものの自動でコントローラに渡してくれるようになります。

掲示板追加時に詳細画面へ移動する

現在掲示板を新規で作ると追加画面のままなので、詳細へリダイレクトするようにします。

まずテスト

        [TestMethod]
        public void PostCreate()
        {
            // モック用意
            var mockset = new Mock<DbSet<BoardEntity>>();
            var mockcontext = new Mock<BoardDbContext>();

            var model = new BoardCreateModel {
                Title = "題名",
                Text = "本文"
            };

            var dummy = new BoardEntity { Id = 1, Title = model.Title, Text = model.Text };
            mockset.As<IDbSet<BoardEntity>>().Setup( m => m.Add( It.IsAny<BoardEntity>() ) ).Returns( dummy );

            mockcontext.Setup( m => m.Boards ).Returns( mockset.Object );

            var controller = new BoardController(mockcontext.Object);
            var result = controller.Create(model) as RedirectResult;
            Assert.IsNotNull( result );

            // Addが呼ばれたかチェック
            mockset.Verify( m => m.Add( It.Is<BoardEntity>( o => o.Title == model.Title && o.Text == model.Text ) ), Times.Once );

            // SaveChangesがよばれたかチェック
            mockcontext.Verify( m => m.SaveChanges(), Times.Once );

            Assert.AreEqual( result.Url, "/Board/Show/1" );
        }

リダイレクトを考慮したテストに変更しました。

Addの返り値のIdを参照してリダイレクトするのでこのような形に。

コントローラも対応

        // POST: Board/Create
        [HttpPost]
        public ActionResult Create(BoardCreateModel data)
        {
            var result = db_.Boards.Add( new BoardEntity {
                Title = data.Title,
                Text = data.Text
            } );
            
            db_.SaveChanges();
            return Redirect("/Board/Show/" + result.Id);
        }

コントローラ側はこんな感じです。

リファクタリングする

これはいろいろ問題点がありますので、目についた部分を2点程リファクタリングします。

  1. テストのモック生成処理が煩雑
  2. BoardControllerにビジネスロジック(DBからの検索とか)が入っている

です。

1に関してはモック生成回りを関数化します。

2に関してはRepositoryクラスというDBの処理を請け負うクラスを用意し利用します。

一度に両方リファクタリングすると不具合の元なので一つずつ細かく修正してテストを実行して確認しましょう。

リファクタリング結果はgithubのコードを確認してみてください。

コメント

  1. […] 入門の書 – Qiita introduction-to-asp-dot-net – ASP.NET勉強会 ASP.Netで0から超簡易掲示板を作る […]

タイトルとURLをコピーしました