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.