سؤال

Im trying to verify that a specific CancellationTokenSource is used as an actual parameter in a method call.

        public void DataVerification(Object sender, EventArgs e)
        {
            _entity.PopulateEntityDataVerificationStage(_view.DataTypeInputs, _view.ColumnNameInputs, _view.InitialRow, _view.FinalRow, _view.CurrencyPair, _view.CsvFilePath, _view.ErrorLogFilePath);

            //...

            CancellationTokenSource tempCsvFileVerificationCancellation = new CancellationTokenSource();

            _source.Source = tempCsvFileVerificationCancellation;

           //Want to verify that TempCsvFileVerificationCancellation.Token is passed into the following method.
            _verify.SetupCsvFileVerification(_entity, tempCsvFileVerificationCancellation.Token);

           //...
        } 

The following is my test:

    [Test]
    public void DataVerification_SetupCsvFileVerification_CorrectInputs()
    {
        Mock<IMainForm> view = new Mock<IMainForm>();

        Mock<IUserInputEntity> entity = new Mock<IUserInputEntity>();

        Mock<ICsvFileVerification> verify = new Mock<ICsvFileVerification>();
        verify.Setup(x => x.SetupCsvFileVerification(It.IsAny<UserInputEntity>(), It.IsAny<CancellationToken>()));

        CancellationTokenSource cts = new CancellationTokenSource();

        Mock<ICancellationTokenSource> source = new Mock<ICancellationTokenSource>();
        source.SetupSet(x => x.Source = It.IsAny<CancellationTokenSource>()).Callback<CancellationTokenSource>(value => cts = value);
        source.SetupGet(x => x.Source).Returns(cts);
        source.SetupGet(x => x.Token).Returns(cts.Token);

        MainPresenter presenter = new MainPresenter(view.Object, entity.Object, verify.Object, source.Object);

        presenter.DataVerification(new object(), new EventArgs());

        verify.Verify(x => x.SetupCsvFileVerification(entity.Object, source.Object.Token));
    }

The error message is as follows:

Expected invocation on the mock at least once, but was never performed: x => x.SetupCsvFileVerification(.entity.Object, (Object).source.Object.Token) No setups configured.

The class represented by source is as follows:

public interface ICancellationTokenSource
{
    void Cancel();

    CancellationTokenSource Source { get; set; }

    CancellationToken Token { get; }
}

public class CancellationTokenSourceWrapper : ICancellationTokenSource
{
    private CancellationTokenSource _source;

    public CancellationTokenSourceWrapper(CancellationTokenSource source)
    {
        _source = source;
    }

    public CancellationTokenSource Source
    {
        get
        {
            return _source;
        }
        set
        {
            _source = value;
        }
    }


    public CancellationToken Token 
    {
        get
        {
            return Source.Token;
        }
    }

    public void Cancel()
    {
        _source.Cancel();
    }
}

When i step through the unit test, cts does get assigned the value of TempCsvFileVerificationCancellation. The Token property in source, returns Source.Token. Im at a loss as to what i have done wrong.

Any pointers/assistance would be appreciated.

Thanks

EDIT

هل كانت مفيدة؟

المحلول

At first blush it looks like it should work but as it does not you might try:

Change the matching conditions on the verify to It.IsAny(), It.IsAny() to see if it is called at all. - If not, debug through the code and see what is happening - If it does match, then it is a matter of the matching

Try entity.Object, It.IsAny() to see if the UserInputEntity is wrong.

If the UserInputEntity is fine then put a callback on the source Setup. It doesn't need to do anything but will allow you to check which values are being used when SetupCsvFileVerification is called.

UPDATE
I think I found it, and it relates to one of my points below. You initialize cts to a value. It needs to have an initial value because the setups

source.SetupGet(x => x.Source).Returns(cts); 
source.SetupGet(x => x.Token).Returns(cts.Token); 

fail without it because it evaluates cts.Token immediately. It means that this will return the token from the CancellationTokenSource defined in the test, not the one defined in the production code (and stored using the callback).

To ensure that you use the new cts value you should change the setups to

source.SetupGet(x => x.Source).Returns(() => cts);
source.SetupGet(x => x.Token).Returns(() => cts.Token);

which will defer evaluation until used i.e. after the callback from the set has executed.

Added Details
The problem is timing of evaluation

Say in the test setup you create a cts X which has a token A.
You then setup the Source to return the cts and the token to return cts.Token
These are evaluted now, the gets are told to return X and A.

When running, the cts is overwritten by the one set using the callback (call it Y with a Token of B) and it is the B value that the Verify uses and thus fails.


By changing the setups to use lambdas we are telling them to use 'whatever value cts is pointing at when you get called' so the sequence is now

Setup up get - do not evaluate CTS or token values Call validation
- set cts using callback
- get source => evaluate, use newly set value (Y)
- get token => evaluate, use newly set value (B)
Verify => compare against B, passes

Other ideas

Does the CancellationTokenSource exist only to support testing?

If so, another approach is to try replacing this with an ICancellationTokenProvider which would replace the new CancellationTokenSource(); call in your production code. This would allow you to more easily inject a specific CancellationToken into the code and thus verify SetupCsvFileVerification().

Minor quibbles - ignore at will

Also, unless you are using a strict behaviour the setup

verify.Setup(x => x.SetupCsvFileVerification(It.IsAny<UserInputEntity>(), 
                                             It.IsAny<CancellationToken>()));

is superfluous. It doesn't return anything, so it is not useful as a stub, and you are explicitly verifying the call later on.

Likewise the initialization of 'cts' to a value is not needed, setting it to null should suffice as the callback on setting the source should populate it.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top