Fakes

Core

There are several so-called isolation frameworks, e.g. the ones providing you with the ability to create a fake object and use it inside your unit tests. The most prominent are Moq FakeItEasy RhinoMock and many more. The framework makes no attempt of creating yet another isolation framework, it simpy provides and abstraction sutiable for the most common unit tests cases especially the ones covered by the fake data substitution and builders which will be discussed in further chapters.

public interface IFake<TFaked> : where TFaked: class    
{                
    IFakeCallback Setup(Expression<Action<TFaked>> expression);
    IFakeCallbackWithResult<TResult> Setup<TResult>(Expression<Func<TFaked, TResult>> expression);    
}

As you can see the main functionality to be used by fake-consuming code is setting up the desired behavior via simple methods.

Setup

There are various options for setting up the required behavior. The Attest framework uses fluent API approach where the callbacks should be chained to achieve the desired effect. Let's consider the following interface:

 public interface IPhasesProvider    
 {        
     Guid[] GetPhasesByGauge(Guid gaugeId);    
 }

We would like to have an instance of faked object which will produce the required callback upon method invocation. This can be done using the following approach:

FakeFactoryContext.Current = new FakeFactory();            
ConstraintFactoryContext.Current = new ConstraintFactory();

Here we set the initial fake providers. The only supported provider is Moq

 var initialSetup = ServiceCallFactory.CreateServiceCall(FakeService);
 var setup = initialSetup.AddMethodCallWithResult<Guid, Guid[]>(
                          t => t.GetPhasesByGauge(It.IsAny<Guid>()),
                          (r, id) => r.Complete(
                                       k => /// provide your callback
                                       ));

Here we create the initial method call template and then start adding method calls with the required callbacks.

var faked = setup.Build();

Finally we call the Build method to get the faked instance which implements the IPhasesProvider interface.

Builders

The main idea behind using fakes is to simulate some kind of behavior during the test and assert the unit/system state. However this concept of simulated/fake behavior can be easily expanded to the app itself to allow modular and independent development. This means that the faked objects should also become stateful to be able to cope with multiple calls during the app lifetime. Additionally they should still expose the very same interface consumed by the app. To sum it up, the fake data builders combine the storage capabilities with the simulated behavior allowing full app development even without real data providers. Let's see an example:

public interface IPhasesProvider
{        
    Guid[] GetPhasesByGauge(Guid gaugeId);
}

public class PhaseDto
{
    public Guid GaugesId { get; set; }       
    public Guid Id { get; set; }        
    public string Key { get; set; }        
    public string Name { get; set; }        
    public double Value { get; set; }    
}

public class PhasesProviderBuilder : FakeBuilderBase<IPhasesProvider>.WithInitialSetup    
{        
    private readonly Dictionary<Guid, List<PhaseDto>> _phases = new Dictionary<Guid, List<PhaseDto>>();        
    public static PhasesProviderBuilder CreateBuilder()
    {            
        return new PhasesProviderBuilder();        
    }     
       
    public void WithPhases(Guid gaugeId, IEnumerable<PhaseDto> phases)
    {            
        if (_phases.ContainsKey(gaugeId) == false)
        {                
            _phases.Add(gaugeId, new List<PhaseDto>());
        }            
        _phases[gaugeId].AddRange(phases);
    }    
        
    protected override IServiceCall<IPhasesProvider> CreateServiceCall(IHaveNoMethods<IPhasesProvider> serviceCallTemplate)        
    {            
        var setup = serviceCallTemplate.AddMethodCallWithResult<Guid, Guid[]>(
                                        t => t.GetPhasesByGauge(It.IsAny<Guid>()),
                                        (r, id) => r.Complete(k => _phases[k].Select(
                                                     t => t.Id).ToArray()));
        return setup;
    }    
}

Here we see an example of state inside the builder which is implemented by _phases field. Additionally the initial setup is implemented by WithPhases method. The two mandatory parts are the implementation of CreateServiceCall method which assigns the actual method calls and the factory method CreateBuilder which is used for dynamic invocations.

This builder is used indirectly during the test by consuming the underlying FakeService and therefore has to remain consistent to ensure proper app simulation.

Data

A typical app consists of several functional layers. In a classic Onion/DDD architecture the central part of every app is the Domain while the Infra layer is an auxiliary one. The Storage functionality is part of the Infra layer as well and should be faimiliar to Domain by abstractions only. This architecture fits perfectly with the fake data providers and fake data provider builders approach. In this case the correspondent module is substituted according to the current configuration.

public class UserDto    
{        
    public string Username { get; set; }        
    public string Fullname { get; set; }    
}

public interface ILoginProvider
{        
    UserDto GetUser(string username, string password);    
}

public class LoginProviderBuilder : FakeBuilderBase<ILoginProvider>.WithInitialSetup    
{        
    private readonly Dictionary<string, UserDto> _users = new Dictionary<string, UserDto>();        
    private readonly Dictionary<string, string> _passwords = new Dictionary<string, string>();        
    
    public static LoginProviderBuilder CreateBuilder() => new LoginProviderBuilder();        
    
    private LoginProviderBuilder()
    {        
    }        
    
    public LoginProviderBuilder WithUser(UserDto user, string password)
    {            
        return WithUserImpl(user, password);        
    }        
    
    public LoginProviderBuilder WithUser(string username, string password)
    {            
        var user = new UserDto
        {
            Username = username,
            Fullname = username
        };            
        return WithUserImpl(user, password);
    }        
    
    private LoginProviderBuilder WithUserImpl(UserDto user, string password)
    {            
        _users.Add(user.Username, user);
        _passwords.Add(user.Username, password);
        return this;
    }       
    
    protected override IServiceCall<ILoginProvider> CreateServiceCall(IHaveNoMethods<ILoginProvider> serviceCallTemplate) => 
        serviceCallTemplate.AddMethodCallWithResult<string, string, UserDto>(
                        t => t.GetUser(It.IsAny<string>(), It.IsAny<string>()),(r, n, p) => r.Complete(GetUser));
                        
    private UserDto GetUser(string username, string password)        
    {            
        if (_passwords.TryGetValue(username, out var p))
        {
            if (p != password)
            {
                throw new AuthenticationException("Wrong password");
            }                
            return _users[username];
        }            
        throw new AuthenticationException("Username not found");        
    }    
}

internal sealed class FakeLoginProvider : FakeProviderBase<LoginProviderBuilder, ILoginProvider>, ILoginProvider    
{        
    private readonly LoginProviderBuilder _loginProviderBuilder;        
    
    public FakeLoginProvider(LoginProviderBuilder loginProviderBuilder,            
                             ILoginContainer loginContainer)
    {            
        _loginProviderBuilder = loginProviderBuilder;            
        foreach (var user in loginContainer.Users)
        {                
            _loginProviderBuilder.WithUser(user.Key, user.Value);            
        }        
    }        
    
    UserDto ILoginProvider.GetUser(string username, string password) =>
                GetService(() => _loginProviderBuilder, b => b)
                .GetUser(username, password);    
}

 internal sealed class Module : ICompositionModule<IServiceCollection>
 {        
     public void RegisterModule(IServiceCollection dependencyRegistrator)
     {            
         dependencyRegistrator.AddSingleton<IAlgorithmProvider, FakeAlgorithmProvider>();
         dependencyRegistrator.AddSingleton(AlgorithmProviderBuilder.CreateBuilder());
     }
 }

Let's have a closer look at this code. You can see here that the IAlgorithmProvider is registered into the DI container and the actual implementation is entirely internal. This is extremely useful when we want to switch between the data layers without changing the actual Domain code. The fake data provider itself is merely a redirection to the underlying fake data provider builder. The fake data provider builder is the only stateful implementation of the abstraction's functionality. You will see in the Testing section how this approach can be used to allow complex data setup and arrange scenarios.

Last updated