ssannandeji / Zenject-2019

Dependency Injection Framework for Unity3D
MIT License
2.53k stars 363 forks source link

Using LazyInject in UnitTests #630

Closed FireDragonGameStudio closed 5 years ago

FireDragonGameStudio commented 5 years ago

Hi, is it possible to use LazyInject within UnitTests? I tried something like the following and _foo in class Bar (line 13) is always null when executing the UnitTest. I already played around with the order of binding and couldn't solve this.

Best regards :)

using System;
using System.Threading.Tasks;
using Zenject;

public class Bar : BarBase, IBar {
    LazyInject<Foo> _foo;

    public Bar(LazyInject<Foo> foo) {
        _foo = foo;
    }

    public async Task<bool> Run() {
        return await _foo.Value.Run();
    }

    public void Register(Action<bool> callback) {
        int a = 5 + 2;
        callback.Invoke(true);
    }
}
public class BarBase {
    public BarBase() { }
}
using System;
using System.Threading.Tasks;

public interface IBar {
    Task<bool> Run();
    void Register(Action<bool> callback);
}
using System.Threading.Tasks;

public class Foo : IFoo {
    public async Task<bool> Run() {
        await Task.Delay(1000);
        return true;
    }
}
using System.Threading.Tasks;

public interface IFoo {
    Task<bool> Run();
}
using Moq;
using NUnit.Framework;
using System;
using Zenject;

[TestFixture]
public class FooBarUnitTest : ZenjectUnitTestFixture {

    [Inject]
    IBar _bar;
    [Inject]
    IFoo _foo;

    [SetUp]
    public void CommonInstall() {

        Container.Bind<IFoo>().To<Foo>().AsSingle();

        Mock<IBar> bar = new Mock<Bar>(_foo).As<IBar>();
        bar.CallBase = true;
        bar.Setup(b => b.Register(It.IsAny<Action<bool>>()))
            .Callback<Action<bool>>(async (action) => {
                action.Invoke(await bar.Object.Run());
            }
        );

        Container.BindInstance(bar.Object).AsSingle();
        Container.Inject(this);
    }

    [Test]
    public void TestInitialValues() {
        bool wasRunning = _bar.Run().GetAwaiter().GetResult();

        Assert.True(wasRunning);
    }
}
svermeulen commented 5 years ago

It looks like you are using moq to create the instance of Bar? You need to use zenject for this. If you create it through zenject then it should inject LazyInject parameters properly

FireDragonGameStudio commented 5 years ago

Thx for your swift reply an suggestion. I'll try it asap and close this issue afterwards :)

FireDragonGameStudio commented 5 years ago

@svermeulen If I use Zenject without moq, the object graph can't be created and the test fails. It either takes forever to fail (maybe some kind of loop) or fails with the error ZenjectException: Unable to resolve 'Foo' while building object with type 'Bar'.

svermeulen commented 5 years ago

Can you post your entire test?

FireDragonGameStudio commented 5 years ago

sure :)

using NUnit.Framework;
using Zenject;

[TestFixture]
public class FooBarUnitTest : ZenjectUnitTestFixture {

    [Inject]
    IBar _bar;

    [SetUp]
    public void CommonInstall() {
        Container.Bind<IFoo>().To<Foo>().AsSingle();
        Container.Bind<IBar>().To<Bar>().AsSingle();
        Container.Inject(this);
    }

    [Test]
    public void TestInitialValues() {
        bool wasRunning = _bar.Run().GetAwaiter().GetResult();

        Assert.True(wasRunning);
    }
}
svermeulen commented 5 years ago

The error message suggests that you need to bind Foo. In your code you are only binding IFoo

FireDragonGameStudio commented 5 years ago

When changing the binding to Foo (like Container.Bind<Foo>().AsSingle(); the test results in an endless loop and doesn't finish.

svermeulen commented 5 years ago

Is it waiting when you call GetResult? I'm guessing zenject has created IBar correctly and it's on to running your code

FireDragonGameStudio commented 5 years ago

Yes, but I'm just waiting for 1 second, before moving on. After calling await Task.Delay(1000); in class Foo it never returns.

svermeulen commented 5 years ago

I think it has something to do with your async code though, not zenject. Maybe check into what SynchronizationContext you're using, since that will determine how code is continued after an await

FireDragonGameStudio commented 5 years ago

Found the issue! The error is caused by Unitys old NUnit version (https://forum.unity.com/threads/async-await-in-unittests.513857/) The execution of async await in UnitTests is... different.

Thank you for pointing me into the right direction :) Correct test looks now like this:

using NUnit.Framework;
using System.Threading.Tasks;
using Zenject;

[TestFixture]
public class FooBarUnitTest : ZenjectUnitTestFixture {

    [Inject]
    IBar _bar;

    [SetUp]
    public void CommonInstall() {
        Container.Bind<Foo>().AsSingle();
        Container.Bind<IBar>().To<Bar>().AsSingle();
        Container.Inject(this);
    }

    [Test]
    public void TestInitialValues() {

        bool wasRunning = false;

        wasRunning = Task.Run(() => _bar.Run()).GetAwaiter().GetResult();

        Assert.True(wasRunning);
    }
}

Best regards :)