Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weird issue using spread operator on IEnumerable<Task> when using mocked methods #1526

Open
sbloomfieldtea opened this issue Dec 19, 2024 · 2 comments
Labels

Comments

@sbloomfieldtea
Copy link

sbloomfieldtea commented Dec 19, 2024

Describe the Bug

I will preface this by saying I think I may be doing something wrong.

If I have multple task objects returned from mocked async methods stored in the result of a linq query and I use the ".." spread operator, each item in the result gets set to null.

If I have multple task objects returned from "real" async methods stored in the result of a linq query and I use the ".." spread operator, each item is a valid task and is not null.

I assume it has something to do with the linq query and a delay in resolving the enumerable? But then why would it only behave this way when the method is mocked?

Steps to Reproduce

Here's an example unit test. Only the last assert "mockedMethodTasksSpread" fails.

    public class MyTestClass
    {
        [TestMethod]
        public async Task TestTaskSpread()
        {
            var idList = new string?[] { "31", null, "33", "34" };

            var myMock = new Mock<IMyInterface>();
            myMock.SetupSequence(_ => _.MyTestAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
                .ReturnsAsync("41")
                .ReturnsAsync("42")
                .ReturnsAsync("43")
                .ReturnsAsync("44");

            var realMethodTasks = idList
                .Where(s => !string.IsNullOrWhiteSpace(s))
                .Select(id => MyRealMethodAsync(id!));

            Task<string?>[] realMethodTasksToArray = realMethodTasks.ToArray();
            Task<string?>[] realMethodTasksSpread = [.. realMethodTasks];

            var mockedMethodTasks = idList
                .Where(s => !string.IsNullOrWhiteSpace(s))
                .Select(id => myMock.Object.MyTestAsync(id!, default));

            Task<string?>[] mockedMethodTasksToArray = mockedMethodTasks.ToArray();
            Task<string?>[] mockedMethodTasksSpread = [.. mockedMethodTasks];

            CollectionAssert.AllItemsAreNotNull(realMethodTasksToArray, "realMethodTasksToArray");
            CollectionAssert.AllItemsAreNotNull(realMethodTasksSpread, "realMethodTasksSpread");

            CollectionAssert.AllItemsAreNotNull(mockedMethodTasksToArray, "mockedMethodTasksToArray");
            CollectionAssert.AllItemsAreNotNull(mockedMethodTasksSpread, "mockedMethodTasksSpread");
        }

        private async Task<string?> MyRealMethodAsync(string id)
        {
            await Task.Delay(10);

            return await Task.FromResult(id);
        }
    }

    public interface IMyInterface
    {
        Task<string?> MyTestAsync(string myString, CancellationToken cancellationToken);
    }    

Expected Behavior

The assert mockedMethodTasksSpread should pass.

Exception with Stack Trace

None

Version Info

Here's most of my csproj with nuget versions:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<IsPackable>false</IsPackable>
		<Platforms>x64</Platforms>
		<IsTestProject>true</IsTestProject>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="FluentAssertions" Version="6.12.1" />
		<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
		<PackageReference Include="Moq" Version="4.20.72" />
		<PackageReference Include="MSTest.TestAdapter" Version="3.6.1" />
		<PackageReference Include="MSTest.TestFramework" Version="3.6.1" />
	</ItemGroup>
</Project>

Additional Info

None

Back this issue
Back this issue

@Asafima
Copy link
Contributor

Asafima commented Jan 6, 2025

Actually it's do to the behavior of the SetupSequence method, which I think is not a bug and working as expected.

  1. Both [..enumerable] and .ToArray() immediately enumerate the sequence (triggering the evaluation of deferred queries).
    This means when you're using the realMethodTasks, every time you enumerate it, whether explicitly (using .ToArray()) or implicitly [..enumerable], the enumerator enumerates over the IEnumerable<Task<string?>>.

  2. The method MyRealMethodAsync(id!) is stateless and simply creates a new task each time it’s called.

  3. For mockedMethodTasks, the mock is stateful, because SetupSequence maintains an internal sequence of return values.
    When the mock is invoked for the first time, it returns "41", then "42", then "43", according to the LINQ query.
    On the second enumeration:

Task<string?>[] mockedMethodTasksSpread = [.. mockedMethodTasks];

Once the sequence is exhausted, subsequent calls return null.

Personally I don't think its bug - It would be weird if the SetupSequence restart it's enumerator.

@sbloomfieldtea
Copy link
Author

I see what you are saying. I will go back and break this into two test methods to be sure the two examples aren't stepping on each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants