2
\$\begingroup\$

This is a follow-up of this question regarding MVC application architecture fit for automatic testing.

I have rewritten the DI code based on the answer provided there and also created some tests to see if I can properly benefit from the newly refactored code. I will repeat here the application structure, so that the question can be understood without reading the original question:

  1. Common assembly - contains generic functionalities, including models definition
  2. Fetch job - daily job that fetches and processes raw article data
  3. Web application - the actual Web application that allows users to see processed data
  4. Test project - an automatic testing project based on NUnit that currently contains only integrative tests (no mocking, no real unit tests)

The actual code:

1) Common DI bindings (for Job and Web App mainly)

public class NinjectCommon
{
    public static void RegisterCommonServices(IKernel kernel)
    {
        kernel.Bind<IUnitOfWork>().To<UnitOfWork>();
        kernel.Bind<IAggregatorContext>().ToFactory(() => new AggregatorContextProvider());

        kernel.Bind<IEntitiesCache>().To<EntitiesCache>().InSingletonScope();
        kernel.Bind<IExtDictionaryParser>().To<ExtDictionaryParser>().InSingletonScope();

        kernel.Bind(typeof(IRepository<>)).To(typeof(Repository<>));
    }
}

2) Repository definition

public interface IRepository
{
}

public interface IRepository<TEntity> : IRepository
    where TEntity : class
{
    IQueryable<TEntity> AllNoTracking { get; }
    IQueryable<TEntity> All { get; }
    TEntity Get(int id);
    void Insert(TEntity entity);
    void Delete(TEntity entity);
    void Update(TEntity entity);
}

public class Repository<T> : IRepository<T> where T : class, new()
{
    private IAggregatorContext _context;

    public Repository(IAggregatorContext context)
    {
        this._context = context;
    }

    public IQueryable<T> All
    {
        get { return _context.Set<T>().AsQueryable(); }
    }

    public IQueryable<T> AllNoTracking
    {
        get { return _context.Set<T>().AsNoTracking(); }
    }

    public T Get(int id)
    {
        return _context.Set<T>().Find(id);
    }

    public void Delete(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
            _context.Set<T>().Attach(entity);
        _context.Set<T>().Remove(entity);
    }

    public void Insert(T entity)
    {
        _context.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        _context.Set<T>().Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }
}

3) Unit of work

public class UnitOfWork : IUnitOfWork
{
    #region Members
    private IAggregatorContext _context;
    #endregion

    #region Properties
    public IRepository<Lexem> LexemRepository { get; private set; }
    public IRepository<Word> WordRepository { get; private set; }
    public IRepository<Synset> SynsetRepository { get; private set; }

    // other repositories come here (removed for brevity)
    #endregion

    #region Constructor
    public UnitOfWork(IAggregatorContext context,
        IRepository<Lexem> lexemRepository, IRepository<Word> wordRepository, IRepository<Synset> synsetRepository,
        /* other repositories params here */)
    {
        this._context = context;

        LexemRepository = lexemRepository;
        WordRepository = wordRepository;
        SynsetRepository = synsetRepository;
    }
    #endregion

    #region Methods
    public IRepository<T> GetRepository<T>()
        where T: class
    {
        Type thisType = this.GetType();
        foreach (var prop in thisType.GetProperties())
        {
            var propType = prop.PropertyType;

            if (!typeof(IRepository).IsAssignableFrom(propType))
                continue;

            var repoType = propType.GenericTypeArguments[0];
            if (repoType == typeof(T))
                return (IRepository<T>) prop.GetValue(this);
        }

        throw new ArgumentException(String.Format("No repository of type {0} found", typeof(T).FullName));
    }

    public void SaveChanges()
    {
        _context.SaveChanges();
    }

    public bool SaveChangesEx()
    {
        return _context.SaveChangesEx();
    }
    #endregion
}

4) Fetch job DI setup

class Program
{
    #region Members
    private static Logger logger = LogManager.GetCurrentClassLogger();
    #endregion

    #region Properties
    private static IUnitOfWork _UnitOfWork { get; set; }
    private static IKernel _Kernel { get; set; }
    private static IFetchJob _FetchJob { get; set; }
    #endregion

    #region Methods
    private static void init()
    {
        // setup DI
        _Kernel = new StandardKernel();
        _Kernel.Load(Assembly.GetExecutingAssembly());

        NinjectCommon.RegisterCommonServices(_Kernel);
        registerServices();

        _UnitOfWork = _Kernel.Get<IUnitOfWork>();
        _FetchJob = _Kernel.Get<IFetchJob>();
    }

    private static void registerServices()
    {
        _Kernel.Bind<INlpUtils>().To<NlpUtils>();

        _Kernel.Bind<IFetchJob>().To<FetchJob>().InSingletonScope();
    }

    static void Main(string[] args)
    {
        init();

        Utils.InitNLogConnection();
        logger.LogEx(LogLevel.Info, "FetchJob started");

        MappingConfig.CreateMappings();

        try
        {
            _FetchJob.FetchArticleData();
        }
        catch (Exception exc)
        {
            logger.LogEx(LogLevel.Fatal, "Global unhandled exception", exc);
        }
        finally
        {
            logger.LogEx(LogLevel.Info, "FetchJob stopped");
        }
    }
    #endregion
}

5) Web application DI setup

public static class NinjectWebCommon 
{
    private static readonly Bootstrapper bootstrapper = new Bootstrapper();

    /// <summary>
    /// Starts the application
    /// </summary>
    public static void Start() 
    {
        DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
        DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
        bootstrapper.Initialize(CreateKernel);
    }

    /// <summary>
    /// Stops the application.
    /// </summary>
    public static void Stop()
    {
        bootstrapper.ShutDown();
    }

    /// <summary>
    /// Creates the kernel that will manage your application.
    /// </summary>
    /// <returns>The created kernel.</returns>
    private static IKernel CreateKernel()
    {
        var kernel = new StandardKernel();
        try
        {
            kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
            kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();

            RegisterServices(kernel);
            return kernel;
        }
        catch
        {
            kernel.Dispose();
            throw;
        }
    }

    /// <summary>
    /// Load your modules or register your services here!
    /// </summary>
    /// <param name="kernel">The kernel.</param>
    private static void RegisterServices(IKernel kernel)
    {
        NinjectCommon.RegisterCommonServices(kernel);

        // other bindings come here
    }        
}

6) Automatic tests projects DI setup

All test classes inherit a base test class that provides some functionality for all test groups:

public abstract class BaseTest
{
    #region Variables
    protected IKernel _Kernel { get; private set; }

    [Inject]
    public IUnitOfWork UnitOfWork { get; private set; }
    #endregion

    #region Constructor
    protected BaseTest()
    {
        _Kernel = new NSubstituteMockingKernel();
        _Kernel.Load(Assembly.GetExecutingAssembly());

        NinjectCommon.RegisterCommonServices(_Kernel);
        RegisterServices();

        UnitOfWork = _Kernel.Get<IUnitOfWork>();

        // used to inject into properties
        _Kernel.Inject(this);
    }
    #endregion

    #region Abstract methods
    [SetUp]
    protected virtual void Init()
    {
        MappingConfig.CreateMappings();
        Utils.InitNLogConnection();
    }

    protected virtual void RegisterServices()
    {
        _Kernel.Bind<INlpUtils>().To<NlpUtils>();
    }

    [TearDown]
    protected abstract void TearDown();
    #endregion
}

7) A test that rebinds to a mockup type that replaces all real type functionality

public class ExtDictionaryParserTest : BaseTest
{
    #region Properties
    [Inject]
    public IExtDictionaryParser ExtDictionaryParser { get; set; }
    #endregion

    #region Overrides
    protected override void TearDown()
    {
    }

    protected override void RegisterServices()
    {
        base.RegisterServices();

        _Kernel.Rebind<IExtDictionaryParser>().To<ExtDictionaryParserMockup>();
    }
    #endregion

    #region Tests
    [Test]
    [Category(Constants.Fast)]
    public void ExtDictionaryParse()
    {
        bool isForeign = false;
        Assert.AreEqual(ExtDictionaryParser.WordFromExtDictionary("pagina", out isForeign), "pagina");
        Assert.IsFalse(isForeign);

        Assert.AreEqual(ExtDictionaryParser.WordFromExtDictionary("pagini", out isForeign), "pagina");
        Assert.IsFalse(isForeign);

        Assert.AreEqual(ExtDictionaryParser.WordFromExtDictionary("pages", out isForeign), "page");
        Assert.IsTrue(isForeign);

        Assert.IsNull(ExtDictionaryParser.WordFromExtDictionary("nonexisting_word", out isForeign));
    }
    #endregion
}

8) A test that replaces a repository return function that is used by another function

public class EntitiesCacheTest : BaseTest
{
    #region Tests
    [Test]
    [Category(Constants.Fast)]
    public void BaseWordExists()
    {
        var dummyList = new List<Lexem>() {
            new Lexem() { Word = "something" },
            new Lexem() { Word = "other" },
            new Lexem() { Word = "nothing" }
        };

        var unitOfWorkSubst = Substitute.For<IUnitOfWork>();
        unitOfWorkSubst.LexemRepository.AllNoTracking.Returns(dummyList.AsQueryable());

        _Kernel.Rebind<IUnitOfWork>().ToConstant(unitOfWorkSubst);
        _Kernel.Rebind<IEntitiesCache>().To<EntitiesCache>().InSingletonScope();

        var entitiesCache = _Kernel.Get<IEntitiesCache>();

        Assert.IsTrue(entitiesCache.BaseWordExists("something"));
        Assert.IsTrue(entitiesCache.BaseWordExists("other"));
        Assert.IsFalse(entitiesCache.BaseWordExists("authentic"));
        Assert.IsFalse(entitiesCache.BaseWordExists("Something"));
        Assert.IsFalse(entitiesCache.BaseWordExists("NonExistent"));
    }
    #endregion
}

My questions is:

am I using correctly DI in the above automated tests? Should I improve something to what is already there? As the application grows, I expect to have hundreds of tests, so managing them correctly is important.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.