Skip to content

Instantly share code, notes, and snippets.

@elkdanger elkdanger/Test Subject Builder Secret
Last active Jan 5, 2018

Embed
What would you like to do?
An implementation of a unit test subject builder, that fluently builds test subjects and injects default dependencies through Moq
public class TestSubjectBuilder<T>
where T : class
{
private readonly IServiceContainer _container = new ServiceContainer();
/// <summary>
/// Gets the service container.
/// </summary>
public IServiceContainer ServiceContainer
{
get { return this._container; }
}
/// <summary>
/// Binds the type TInput to the specified instance.
/// </summary>
/// <typeparam name="TInput">The binding type</typeparam>
/// <param name="instance">The instance to bind to the type</param>
public TBuilder Bind<TInput>(TInput instance)
{
if (instance == null)
throw new ArgumentNullException("An instance must be provided");
_container.AddService(typeof(TInput), instance);
return _this;
}
/// <summary>
/// Builds the provider instance
/// </summary>
public override T Build()
{
var instanceType = typeof(T);
var instance = CreateInstance(instanceType, instanceType);
// Also run through each property that is attributed with the Ninject "Inject" attribute
var properties = instanceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetCustomAttribute(typeof(InjectAttribute)) != null).ToList();
properties.ForEach(p =>
{
object serviceInstance = GetInstanceToInject(this._container, p.PropertyType);
p.SetValue(instance, serviceInstance);
});
return instance as T;
}
/// <summary>
/// Builds a mocked version of the specified type
/// </summary>
public override Mock<T> BuildMock()
{
var instance = CreateInstance(typeof(Mock<T>), typeof(T)) as Mock<T>;
if (instance == null)
return null;
object mockInstance = instance.Object;
// Also run through each property that is attributed with the Ninject "Inject" attribute
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetCustomAttribute(typeof(InjectAttribute)) != null).ToList();
properties.ForEach(p =>
{
object serviceInstance = GetInstanceToInject(this._container, p.PropertyType);
p.SetValue(mockInstance, serviceInstance);
});
return instance;
}
/// <summary>
/// Builds a mocked version of the specified type, optionally specifying whether or not the base method should be called.
/// </summary>
public Mock<T> BuildMock(bool callbase)
{
var mock = this.BuildMock();
mock.CallBase = callbase;
return mock;
}
/// <summary>
/// Creates the required type instance.
/// </summary>
/// <param name="instanceType">Type of the instance.</param>
/// <param name="subjectType">Type of the subject.</param>
protected virtual object CreateInstance(Type instanceType, Type subjectType)
{
// Assume one constructor
var constructors = subjectType.GetConstructors();
if (constructors.Length != 1)
{
throw new InvalidOperationException("This builder only supports construction of types which have exactly one constructor");
}
var constructor = constructors[0];
var parameters = constructor.GetParameters();
var arguments = new List<object>();
// Run through each argument on the constructor and create mocks for each one. For each, where there is a binding
// in _container, use that instead.
foreach (var param in parameters)
{
var paramType = param.ParameterType;
var instance = GetInstanceToInject(this._container, paramType);
arguments.Add(instance);
}
var result = Activator.CreateInstance(instanceType, arguments.ToArray());
return result;
}
/// <summary>
/// Gets the instance to inject.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="objectType">Type of the object.</param>
/// <returns></returns>
private static object GetInstanceToInject(IServiceProvider container, Type objectType)
{
object instance;
instance = container.GetService(objectType);
if (instance != null) return instance;
var mockType = typeof(Mock<>).MakeGenericType(objectType);
var mock = Activator.CreateInstance(mockType);
var objectProperty = GetLowestProperty(mockType, "Object");
instance = objectProperty.GetMethod.Invoke(mock, null);
return instance;
}
// Thanks to Jon Skeet O_o
private static PropertyInfo GetLowestProperty(Type type, string name)
{
while (type != null)
{
var property = type.GetProperty(name, BindingFlags.DeclaredOnly |
BindingFlags.Public |
BindingFlags.Instance);
if (property != null)
{
return property;
}
type = type.BaseType;
}
return null;
}
}
@elkdanger

This comment has been minimized.

Copy link
Owner Author

elkdanger commented Nov 6, 2014

Facilitates the creation of test subjects for unit testing, by creating instances of classes. By default, it creates instances by looking and the constructor and creating a mock using Moq for each parameter. You can bind other instances to parameters to override their creation, specifying only the instances you're interested in for any given test.

Restrictions

  • Only currently works with types that contain exactly one constructor.
  • Only currently works with Moq

Examples

var testSubject = new TestSubjectBuilder<UserService>().Build();

// Login controller might accept 3 instances as constructor arguments, but I'm only interested in IUserService..
// The rest will have mocks automatically created by Moq and used on my behalf.
var serviceMock = new Mock<IUserService>();
var controller = new TestSubjectBuilder<LoginController>()
.Bind<IUserService>(serviceMock.Object)
.Build();

// Also special-cases for creating Mocks
var userServiceMock = new TestSubjectBuilder<Mock<UserService>>()
    .Bind<ISomeDependency>(new TestingDependency())
    .Bind<User>(new TestUser { Username = "testuser" })
    .Build();

userServiceMock.Setup(s => ....).Returns(...);
@Boriszn

This comment has been minimized.

Copy link

Boriszn commented Jan 5, 2018

Hi Steve.
Thanks a lot. This idea and solution looks good. But it doesn't compiles.

  • What is _this ?
  • What IServiceContainer ? Is it LightInject or Ninject
  • What is InjectAttribute ?

Could you please help me get this code to work ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.