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:
- Common assembly - contains generic functionalities, including models definition
- Fetch job - daily job that fetches and processes raw article data
- Web application - the actual Web application that allows users to see processed data
- 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.