2

ORM に NHibernate を使用する MVC アプリがあります。各コントローラーは ISession 構築パラメーターを受け取り、ドメイン モデル オブジェクトに対して CRUD 操作を実行するために使用されます。例えば、

public class HomeController : Controller
{
    public HomeController(ISession session)
    {
        _session = session;
    }
    public ViewResult Index(DateTime minDate, DateTime maxDate)
    {
        var surveys = _session.CreateCriteria<Survey>()
                              .Add( Expression.Like("Name", "Sm%") )
                              .Add( Expression.Between("EntryDate", minDate, maxDate) )
                              .AddOrder( Order.Desc("EntryDate") )
                              .SetMaxResults(10)
                              .List<Survey>();

        // other logic that I want to unit test that does operations on the surveys variable
        return View(someObject);
    }
    private ISession _session;
}

MoqまたはRhinoMocksを使用してISessionオブジェクトをモックすることにより、実際にデータベースにアクセスすることなく、このコントローラーを単独で単体テストしたいと思います。ただし、単体テストで ISession インターフェイスをモック化するのは非常に困難です。これは、多数の呼び出しを連鎖させる流暢なインターフェイスを介して使用されるためです。

1 つの代替手段は、リポジトリ パターンを介して ISession の使用をラップすることです。次のようなラッパークラスを書くことができます:

public interface IRepository
{
   List<Survey> SearchSurveyByDate(DateTime minDate, DateTime maxDate);
}

public class SurveyRepository : IRepository
{
    public SurveyRepository(ISession session)
    {    
        _session = session;
    }
    public List<Survey> SearchSurveyByDate(DateTime minDate, DateTime maxDate)
    {
        return _session.CreateCriteria<Survey>()
                          .Add( Expression.Like("Name", "Sm%") )
                          .Add( Expression.Between("EntryDate", minDate, maxDate) )
                          .AddOrder( Order.Desc("EntryDate") )
                          .SetMaxResults(10)
                          .List<Survey>();
    }
    private ISession _session;
}

次に、ISession 引数の代わりに IRepository コンストラクター引数を取るようにコントローラーを書き直すことができます。

public class HomeController : Controller
{
    public HomeController(IRepository repository)
    {
        _repository = repository;
    }

    public ViewResult Index(DateTime minDate, DateTime maxDate)
    {
        var surveys = _repository.SearchSurveyByDate(minDate, maxDate);
         // other logic that I want to unit test that does operations on the surveys variable
        return View(someObject);
    }
    private IRepository _repository;
}

この 2 番目のアプローチは、単一のメソッド呼び出しであるため、IRepository インターフェイスは ISession インターフェイスよりもモックするのがはるかに簡単であるため、単体テストがはるかに簡単になります。ただし、次の理由から、このルートをたどりたくありません。

1) 単体テストを簡単にするためだけに、まったく新しい抽象化レイヤーを作成し、さらに複雑にすることは、本当に悪い考えのように思えます。

2) ISession インターフェイスはすでにリポジトリのようなインターフェイスであるため、nHibernate でリポジトリ パターンを使用するという考えに反対するコメントがたくさんあります。(特に Ayende の投稿をここここで参照してください)、私はこの解説に同意する傾向があります。

私の質問は、ISession オブジェクトをモックすることで初期実装を単体テストできる方法はありますか? そうでない場合、リポジトリ パターンを使用して ISession クエリをラップする唯一の手段ですか、それともこれを解決できる他の方法がありますか?

4

3 に答える 3

3

オレンはよく徘徊する傾向があります。彼は以前、リポジトリと作業単位の強力な支持者でした。彼はおそらく再びそれに戻るでしょうが、要件のセットが異なります.

リポジトリには非常に具体的な利点がいくつかありますが、Oren のコメントでは解決策がまったく見つかりませんでした。また、彼が推奨するものには、独自の制限と問題があります。ある問題を別の問題と交換しているように感じることがあります。また、Web アプリを維持しながら、Web サービスやデスクトップ アプリケーションなど、同じデータのさまざまなビューを提供する必要がある場合にも適しています。

そうは言っても、彼にはたくさんの良い点があります。それらに対する適切な解決策がまだあるかどうかはわかりません。

リポジトリは、高度なテスト主導のシナリオでは依然として非常に役立ちます。特定の ORM または永続化レイヤーに固執するかどうかがわからず、別のレイヤーと交換したい場合にも役立ちます。

Oren のソリューションは、nHimbernate をより緊密にアプリに結び付ける傾向があります。それは多くの状況では問題にならないかもしれませんが、他の状況では問題になるかもしれません。

専用のクエリ クラスを作成するという彼のアプローチは興味深いものであり、CQRSへの第一歩のようなものであり、より優れたトータル ソリューションとなる可能性があります。しかし、ソフトウェア開発は依然として、科学というよりはアートやクラフトに近いものです。私たちはまだ学んでいます。

于 2012-05-27T05:12:55.463 に答える
2

ISession をモックするのではなく、SQLite を利用するベース フィクスチャからテストを継承することを検討しましたか?

public class FixtureBase
{
    protected ISession Session { get; private set; }
    private static ISessionFactory _sessionFactory { get; set; }
    private static Configuration _configuration { get; set; }

    [SetUp]
    public void SetUp()
    {
        Session = SessionFactory.OpenSession();
        BuildSchema(Session);
    }

    private static ISessionFactory SessionFactory
    {
        get
        {
           if (_sessionFactory == null)
           {
                var cfg = Fluently.Configure()
                    .Database(FluentNHibernate.Cfg.Db.SQLiteConfiguration.Standard.ShowSql().InMemory())
                    .Mappings(configuration => configuration.FluentMappings.AddFromAssemblyOf<Residential>())
                    .ExposeConfiguration(c => _configuration = c);

                _sessionFactory = cfg.BuildSessionFactory();
           }

            return _sessionFactory;
        }
    }

    private static void BuildSchema(ISession session)
    {
        var export = new SchemaExport(_configuration);
        export.Execute(true, true, false, session.Connection, null);
    }

    [TearDown]
    public void TearDownContext()
    {
        Session.Close();
        Session.Dispose();
    }


}
于 2012-05-26T23:46:30.080 に答える
2

名前付きクエリ メソッドを使用してリポジトリを導入しても、システムが複雑になることはありません。実際、複雑さが軽減され、コードの理解と保守が容易になります。元のバージョンを比較:

public ViewResult Index(DateTime minDate, DateTime maxDate)
{
    var surveys = _session.CreateCriteria<Survey>()
                          .Add(Expression.Like("Name", "Sm%"))
                          .Add(Expression.Between("EntryDate", minDate, maxDate))
                          .AddOrder(Order.Desc("EntryDate"))
                          .SetMaxResults(10)
                          .List<Survey>();

     // other logic which operates on the surveys variable
     return View(someObject);
}

率直に言って、あなたのメソッドの実際のロジックにたどり着く前に、すべてのメモリスロットがすでに占有されていました。どの基準を作成しているか、どのパラメーターを渡しているか、どの値が返されているかを読者が理解するには時間がかかります。そして、コード行間でコンテキストを切り替える必要があります。データ アクセスと Hibernate の観点から考え始めた後、突然、ビジネス ロジック レベルに戻ります。また、調査を日付で検索する必要がある場所が複数ある場合はどうすればよいでしょうか? この譜表をすべて複製しますか?

そして今、私はリポジトリでバージョンを読んでいます:

public ViewResult Index(DateTime minDate, DateTime maxDate)
{
    var surveys = _repository.SearchSurveyByDate(minDate, maxDate);
    // other logic which operates on the surveys variable
    return View(someObject);
}

ここで何が起こっているのかを理解するのに何の努力も必要ありません。このメソッドには、単一の責任と単一レベルの抽象化があります。すべてのデータ アクセス関連のロジックがなくなりました。クエリ ロジックは別の場所で複製されません。実際、私はそれがどのように実装されているかは気にしません。このメソッドの主な目的がsome other logic.

もちろん、ビジネス ロジックの単体テストを簡単に記述できます (また、TDD リポジトリを使用している場合は、実際にデータ アクセス ロジックを記述する前にコントローラをテストできます。また、リポジトリの実装を記述し始めるときに、リポジトリ インターフェイスはすでに設計されています):

[Test]
public void ShouldDoOtherLogic()
{
    // Arrange
    Mock<ISurveryRepository> repository = new Mock<ISurveryRepository>();
    repository.Setup(r => r.SearchSurveyByDate(minDate, maxDate))
              .Returns(surveys);

    // Act
    HomeController controller = new HomeController(repository.Object);
    ViewResult result = controller.Index(minDate, maxDate);

    // Assert
}

ところで、メモリ内データベースの使用は受け入れテストには適していますが、単体テストにはやり過ぎだと思います。

また、NHibernate 3.0 のNHibernate Lambda Extensionsまたは QueryOver も見てください。これは、文字列の代わりに式を使用して条件を作成します。一部のフィールドの名前を変更しても、データ アクセス コードは壊れません。

また、最小値と最大値のペアを渡すためのRangeも見てください。

于 2012-05-28T08:51:12.963 に答える