4

When Fitnesse instantiates a fixture, it looks for a default public constructor.

However, I would like to constructor inject whatever application services I want to consume in the fixture.

I.e. I would like to write my fixture this this...

public class MyColumnFixture : ColumnFixture {
    private readonly IApplicationService _applicationService;

    public ManualExitSetupFixture(IApplicationService applicationService) {
        _applicationService = applicationService;
    }

    public void DoStuff(string arg1) {
        _applicationService.DoStuff(arg1);
    }
}

The best I've managed to come up with so far is to expose the container as a singleton (see below). But there has to be a better way. Autofac integrates so nicely with so many of the other technologies we use.

public class AutofacIntegration {
    private static IContainer _container;

    public static IContainer Container {
        get {
           if (_container == null) {
              var builder = new ContainerBuilder();
              builder.RegisterModule<MyApplicationModule>();
               _container = builder.Build();                    
           }
           return _container;
         }
    }
}

public class MyColumnFixture : ColumnFixture {
    private readonly IApplicationService _applicationService;

    public ManualExitSetupFixture() {
        _applicationService = AutofacIntegration.Container.Resolve<IApplicationService>();
    }

    public void DoStuff(string arg1) {
        _applicationService.DoStuff(arg1);
    }
}

EDIT: Including more detail in my attempts to make this work based on assistance from Mike...

I have created a class which is copy-pasted from the decompiled code for CreateDefault decompiled from Fitsharp...

public class CreateDefault<T, P> : Operator<T, P>, CreateOperator<T> where P : class, Processor<T>
{
    public bool CanCreate(NameMatcher memberName, Tree<T> parameters)
    {
        return true;
    }

    public TypedValue Create(NameMatcher memberName, Tree<T> parameters)
    {
        ...
    }
}

...and registered this in the SuiteConfig.xml...

<suiteConfig>
    <ApplicationUnderTest>
        <AddAssembly>..\..\Services\Win\MyProj.Fitnesse\MyProj.Fitnesse\bin\Debug\MyProj.Fitnesse.dll</AddAssembly>
     </ApplicationUnderTest>
     <Fit.Operators>
         <Add>MyProj.Fitnesse.CreateDefault`2</Add>
     </Fit.Operators>
</suiteConfig>

...this gives the following exception as it tries to load in my CreateDefault<,> class.

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> fitSharp.Machine.Exception.CreateException: Constructor with 0 parameter(s) failed for type 'MyProj.Fitnesse.CreateDefault`2'. ---> System.ArgumentException: Cannot create an instance of MyProj.Fitnesse.CreateDefault`2[T,P] because Type.ContainsGenericParameters is true.
   at System.RuntimeType.CreateInstanceCheckThis()
   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark)
   at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
   at System.Reflection.Assembly.CreateInstance(String typeName, Boolean ignoreCase, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
   at fitSharp.Machine.Engine.RuntimeType.CreateInstance()
   at fitSharp.Machine.Engine.CreateDefault`2.CreateWithoutParameters(RuntimeType runtimeType)
   --- End of inner exception stack trace ---
   at fitSharp.Machine.Engine.CreateDefault`2.CreateWithoutParameters(RuntimeType runtimeType)
   at fitSharp.Machine.Engine.CreateDefault`2.Create(NameMatcher memberName, Tree`1 parameters)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at fitSharp.Machine.Engine.MethodMember.TryInvoke(Object[] parameters)
   at fitSharp.Machine.Engine.ReflectionMember.Invoke(Object[] parameters)
   at fitSharp.Machine.Engine.ProcessorBase`2.Operate[O](Object[] parameters)
   at fitSharp.Machine.Engine.Operators`2.Add(String operatorName)
   --- End of inner exception stack trace ---
   at fitSharp.Machine.Engine.ProcessorExtension.InvokeWithThrow[T](Processor`1 processor, TypedValue instance, MemberName memberName, Tree`1 parameters)
   at fitSharp.Machine.Application.SuiteConfiguration.LoadNode(String typeName, XmlNode methodNode)
   at fitSharp.Machine.Application.SuiteConfiguration.LoadXml(String configurationXml)
   at fitSharp.Machine.Application.ArgumentParser.InvokeArgumentHandler(String switch, String argumentValue)
   at fitSharp.Machine.Application.ArgumentParser.Parse(IList`1 commandLineArguments)
   at fitSharp.Machine.Application.Shell.Run(IList`1 commandLineArguments)

EDIT: Big thanks to Mike, this is now working a treat.

The implementation is really quite simple, and I'm sure it can be improved upon, but here's a quick how-to...

  1. Define a custom CreateOperator
  2. Create the SuiteConfig.xml to point to the dll and load the custom create operator
  3. Specify the SuiteConfig when launching the runner
  4. Register the fixtures

My CreateOperator code...

public class AutofacCreateOperator : CellOperator, CreateOperator<Cell>
{
    private static IContainer _container;

    public AutofacCreateOperator()
    {
        var builder = new ContainerBuilder();
        builder.RegisterModule<FitnesseModule>();
        _container = builder.Build();
    }

    public bool CanCreate(NameMatcher memberName, Tree<Cell> parameters)
    {
        return _container.ComponentRegistry.IsRegistered(new TypedService(Type.GetType(memberName.MatchName)));
    }

    public TypedValue Create(NameMatcher memberName, Tree<Cell> parameters)
    {
        return new TypedValue(_container.Resolve(Type.GetType(memberName.MatchName)));
    }
}
NeedHack
  • 2,885
  • 3
  • 28
  • 42
  • Another option would be to use Property Injection. Would greatly reduce your code and you would still have your dependencies available when the instance was created and delivered to you for use. – Johnathon Sullinger Feb 27 '15 at 16:29

1 Answers1

1

This can be done - you need to write a custom class to handle the creation operation for fitSharp. It will implement this interface:

public interface CreateOperator<T> {
    bool CanCreate(NameMatcher memberName, Tree<T> parameters);
    TypedValue Create(NameMatcher memberName, Tree<T> parameters);
}

Then you tell fitSharp to use this class in the suite configuration file:

<suiteConfig>
    <Fit.Operators>
        <Add>My.Create.Operator</Add>
    </Fit.Operators>
    ...
</suiteConfig>

As a stating point, here's a very simple create operator. You'd modify this to do the AutoFac injection.

public class TestCreateOperator: CellOperator, CreateOperator<Cell> {
  public bool CanCreate(NameMatcher memberName, Tree<Cell> parameters) {
    return memberName.Matches("testname");
  }
  public TypedValue Create(NameMatcher memberName, Tree<Cell> parameters) {
    return new TypedValue("mytestname");
  }
}

FYI, here's the built-in class that does fixture creation.

public class CreateDefault<T,P>: Operator<T, P>, CreateOperator<T> where P: class, Processor<T> {
    public bool CanCreate(NameMatcher memberName, Tree<T> parameters) {
        return true;
    }

public TypedValue Create(NameMatcher memberName, Tree<T> parameters) {
    var runtimeType = Processor.ApplicationUnderTest.FindType(memberName);
    return parameters.Branches.Count == 0
                 ? CreateWithoutParameters(runtimeType)
                 : CreateWithParameters(parameters, runtimeType);
}

static TypedValue CreateWithoutParameters(RuntimeType runtimeType) {
    try {
        return runtimeType.CreateInstance();
    }
    catch (System.Exception e) {
        throw new CreateException(runtimeType.Type, 0, e.InnerException ?? e);
    }
}

TypedValue CreateWithParameters(Tree<T> parameters, RuntimeType runtimeType) {
    RuntimeMember member = runtimeType.GetConstructor(parameters.Branches.Count);
    object[] parameterList = new ParameterList<T>(Processor).GetParameterList(TypedValue.Void, parameters, member);
        try {
            return member.Invoke(parameterList);
        }
        catch (System.Exception e) {
            throw new CreateException(runtimeType.Type, parameterList.Length, e.InnerException ?? e);
        }
    }
}
Mike Stockdale
  • 5,201
  • 3
  • 27
  • 33
  • I am still finding this confusing. Do I need to declare a generic implementation for CreateOperator? I cannot find a single working example anywhere on the web. – NeedHack Feb 18 '15 at 15:30
  • If I make my object factory generic, I get "System.ArgumentException: Cannot create an instance of MyProject.Fitnesse.AutofacIntegration.MyFactory`1[T] because Type.ContainsGenericParameters is true." – NeedHack Feb 18 '15 at 15:57
  • Thank you for your assitance Mike, I have added more details to my post. – NeedHack Feb 19 '15 at 09:50
  • You're right - we can't load a generic class with a suiteConfig entry. I need to make a change to FitSharp to expose another method to do that. I'll try and get it done this weekend. – Mike Stockdale Feb 19 '15 at 15:55
  • Wow, such dedication to the cause. I guess I can repay in a small way by field testing it. Thanks Mike. – NeedHack Feb 20 '15 at 10:41
  • I looked more closely and we don't need a generic class for a Fit create operator. See the sample I added to the answer above. – Mike Stockdale Feb 22 '15 at 21:35
  • Thanks 10e6 for your help Mike - this is now working! – NeedHack Feb 27 '15 at 16:21
  • Great! If you can, please blog your solution to help others in the future. – Mike Stockdale Feb 27 '15 at 17:58