gravplats / crowbar

[unmaintained] An application testing library for ASP.NET MVC 3 and 4
MIT License
7 stars 5 forks source link

Configuring AppProxy with an instance of a dependency #2

Closed marcusoftnet closed 10 years ago

marcusoftnet commented 11 years ago

Hi Mikael Mattias,

I think you're onto something awesome here. Just had a great time converting my Nancy-tests (see this http://www.marcusoft.net/2013/04/PushTheHowDown.html and code here: https://github.com/marcusoftnet/CukeEnvySkillsmatterDemo) to your framework. Work just fine and I'm (almost) happy.

The thing I'm struggling with now is to rebind my dependency to one that I want to supply at run time.

Here's my code, in a wrapper to the controller-class:

public class BankModuleWrapper
    {
       public BankModuleWrapper(ICashDispenser cashDispenser)
        {
            //_browser = new Browser(with =>
            //    {
            //        with.Dependency<ICashDispenser>(cashDispenser);
            //    });
        }
}

Ok - I've left some Nancy-testing code in there. The thing that i want to mimic in Crowbar is the c# with.Dependency<ICashDispenser>(cashDispenser);

So this class (BankModuleWrapper) will be created for every test, and the ICashDispenser I'm supplying here will be asserted against in other parts of the code.

You have examples on creating AppProxies (awesome work on the samples, btw) that always create new instances and that will not work for me since I will do assertions against that instance at other place.

Here's one of your examples

public class AppProxy : MvcApplicationProxyBase<App, AppProxyContext>
    {
        protected override void OnApplicationStart(App application, string testBaseDirectory)
        {
            application.Kernel.Rebind<IExternalRequestAsync>()
                .ToConstant(new ExternalRequestStub(testBaseDirectory));
        }

        protected override AppProxyContext CreateContext(App application, string testBaseDirectory)
        {
            return new AppProxyContext();
        }
    }

Do you get what I mean? My first naive attempt was to take the ICashDispenser as a constructor parameter to the AppProxy, but then i don't know how to create the AppProxy, initalized with the ICashDispenser instance.

Any ideas on how to solve this? Is there something I don't understand/are doing wrong?

mrydengren commented 11 years ago

I've only looked at this briefly. I kind of understand what you wish to achieve. However, when looking at the mentioned repository I couldn't see any ASP.NET MVC/Crowbar related code.

Anyway, I'm off to Capital City and leetspeak. If you're going to attend the conference track me down and we'll have a look at it together. Otherwise I'll address this next week if you can provide some example code.

Oh, and it's Mattias by the way. :-)

marcusoftnet commented 11 years ago

Ha - sorry Mattias!

Good luck on your speech! You'll do great.

The repo is just for reference - i want to achive the same thing, but in Crowbar.

Take it when you've cooled down from leeting..

mrydengren commented 11 years ago

I was only attending, not speaking.

Anyway, I believe what you're trying to do is feasible but there will most likely be dragons.

The AppProxy instance is created by the .NET framework in MvcApplicationProxyFactory. Unfortunately you won't be able to supply any arguments to it.

I guess your best bet is to inject your dependency into the application in the Application.Execute((client, context) => { /* ... */ }); method. Thus, you should define your own custom AppProxy and AppProxyContext.

public class App : HttpApplication
{
    public IKernel Kernel { get { /* return kernel from somewhere; */ }

    /* ... */
}

public class AppProxyContext : IDisposable
{
    public AppProxyContext(IKernel kernel)
    {
        Kernel = kernel;
    }

    public IKernel Kernel {get; private set; }

    public void Dispose() { }
}

public class AppProxy : MvcApplicationProxyBase<App, AppProxyContext>
{
    protected override AppProxyContext CreateContext(App application, string testBaseDirectory)
    {
        return new AppProxyContext(application.Kernel);
    }
}

public class BankWrapper
{
    private readonly ICashDispenser dispenser;

    public BankWrapper(ICashDispenser dispenser)
    {
        this.dispenser = dispenser;
    }

    public void Withdraw(string accountNo, stirng pinCode, int amount)
    {
        // I've purposely left out where the application proxy is instantiated.
        // I'll leave that as an exercise for you to figure out ;-).
        Application.Execute((client, context) => {
            context.Kernel.Rebind<ICashDispenser>().ToConstant(dispenser);

            // post to server ...
        });
    }
}

Note that the code in Application.Execute() method runs in a different app domain than that of the code in the BankWrapper class, which means you're crossing application domain boundaries when you're rebinding the dispenser.

marcusoftnet commented 11 years ago

Ah - cool. I'll look into that and come back to you with the results.

Thanks so far

marcusoftnet commented 11 years ago

That structure will to the job, but I'm still running into problems I'm afraid... I'm might be to slow or old (probably) for this.

Here's my project (now on github): https://github.com/marcusoftnet/CukeEnvyCrowbar and I run into problems in
the BankControllerWrapper.cs on line 22, which produce the following stacktrace:

Successfully withdrawal from an account in credit
System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation.
  ----> System.MissingMethodException : No parameterless constructor defined for this object.

Server stack trace: 
   at System.RuntimeMethodHandle.SerializationInvoke(IRuntimeMethodInfo method, Object target, SerializationInfo info, ref StreamingContext context)
   at System.Runtime.Serialization.ObjectManager.CompleteISerializableObject(Object obj, SerializationInfo info, StreamingContext context)
   at System.Runtime.Serialization.ObjectManager.FixupSpecialObject(ObjectHolder holder)
   at System.Runtime.Serialization.ObjectManager.DoFixups()
   at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   at System.Runtime.Remoting.Channels.CrossAppDomainSerializer.DeserializeObject(MemoryStream stm)
   at System.Runtime.Remoting.Messaging.SmuggledMethodCallMessage.FixupForNewAppDomain()
   at System.Runtime.Remoting.Channels.CrossAppDomainSink.DoDispatch(Byte[] reqStmBuff, SmuggledMethodCallMessage smuggledMcm, ref SmuggledMethodReturnMessage smuggledMrm)
   at System.Runtime.Remoting.Channels.CrossAppDomainSink.DoTransitionDispatchCallback(Object[] args)

Exception rethrown at [0]: 
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(ref MessageData msgData, Int32 type)
   at Crowbar.ProxyBase`2.Process(SerializableDelegate`1 script)
   at CrowbarDemos.Specs.Support.Wrappers.BankControllerWrapper.Withdraw(String accountNo, String pinCode, Int32 amount) in BankControllerWrapper.cs: line 22
   at CrowbarDemos.Specs.Support.Driver_FullStack_InMemory.Withdraw(Int32 amount) in Driver_FullStack_InMemory.cs: line 44
   at CrowbarDemos.Specs.Steps.WithdrawSteps.b(Int32 amount) in WithdrawSteps.cs: line 28
   at TechTalk.SpecFlow.Bindings.BindingInvoker.InvokeBinding(IBinding binding, IContextManager contextManager, Object[] arguments, ITestTracer testTracer, ref TimeSpan duration)
   at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments)
   at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(StepInstance stepInstance)
   at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.OnAfterLastStep()
   at CrowbarDemos.Specs.Specs.CashWithdrawalFeature.ScenarioCleanup() in Withdraw.feature.cs: line 0
   at CrowbarDemos.Specs.Specs.CashWithdrawalFeature.SuccessfullyWithdrawalFromAnAccountInCredit() in Withdraw.feature: line 7
--MissingMethodException
   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, ref Boolean canBeCached, ref RuntimeMethodHandleInternal ctor, ref Boolean bNeedSecurityCheck)
   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, ref StackCrawlMark stackMark)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, ref StackCrawlMark stackMark)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic)
   at System.Activator.CreateInstance(Type type)
   at Crowbar.SerializableDelegate`1.AnonymousClassWrapper..ctor(SerializationInfo info, StreamingContext context)

but I'm still puzzled as to which object that haven't got a parameterless constructor. I even added one to the AppProxyContext.cs. Can you help me out here?

Am I approaching this the wrong way?

mrydengren commented 11 years ago

I haven't tried the code but based on the stack trace I'm guessing that you're running into a problem due to the fact that you're crossing app domain boundaries. If you're passing types between app domain boundaries they need to be serializable. Thus, try making InMemoryCashDispenser serializable.

marcusoftnet commented 11 years ago

Hi,

tried that and I'm sorry but it still doesn't work. Just for laughs I tried to have my `InMemoryCashDispenserinherit fromMarshalByRefObject`` but to no effect.

I'm out in the dark here.

mrydengren commented 11 years ago

Have a look at CrowbarException. It's marshalled between app domains. My guess would be that you need a constructor with same signature.