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\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.