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

Different return values for same method and arguments #262

Open
hanshsieh opened this issue Feb 10, 2023 · 8 comments
Open

Different return values for same method and arguments #262

hanshsieh opened this issue Feb 10, 2023 · 8 comments

Comments

@hanshsieh
Copy link

If a method is called multiple times with the same arguments, how to let different calls return different values?
For example, let's say I have a class:

public class Calendar
{
    public int SumRandom()
    {
        var r = new Random();
        return r.Next(1, 10) + r.Next(1, 10);
    }
}

I want the 1st call to r.Next(1, 10) returns 7 and the 2nd call returns 8.
One of the way I found is like below:

[Test]
public void TestSumRandom()
{
    var randomValue1 = 7;
    var randomValue2 = 8;
    var fake = new Fake<Calendar>();

    var sut = fake.Rewrite(f => f.SumRandom());
    int randomCount = 0;
    sut.Replace((Random r) => r.Next(1, 10))
        .When(() => {
            if (randomCount == 0)
            {
                randomCount++;
                return true;
            }
            return false;
        })
        .Return(randomValue1);
    sut.Replace((Random r) => r.Next(1, 10))
        .When(() => {
            if (randomCount == 1)
            {
                randomCount++;
                return true;
            }
            return false;
        })
        .Return(randomValue2);

    Assert.That(sut.Execute(), Is.EqualTo(randomValue1 + randomValue2));
}

But it's a little tedious. Is there a simpler way to achieve it?

@Serg046
Copy link
Owner

Serg046 commented Feb 28, 2023

No simplier way. The library is designed to cover very corner cases that are not coverable by classic mocking libraries. So here the focus is to prefer universe to convinience. However, you can still refactor code to look more elegant, e.g.

[Test]
public void TestSumRandom()
{
    var randomValue1 = 7;
    var randomValue2 = 8;
    var fake = new Fake<Calendar>();

    var sut = fake.Rewrite(f => f.SumRandom());
    int randomCount = 0;
    sut.Replace((Random r) => r.Next(1, 10))
        .When(() => randomCount++ == 0)
        .Return(randomValue1);
    sut.Replace((Random r) => r.Next(1, 10))
        .When(() => randomCount++ == 1)
        .Return(randomValue2);

    Assert.That(sut.Execute(), Is.EqualTo(randomValue1 + randomValue2));
}

Looks elegant enough to me. Another way to make it better is to introduce some extensions methods on your project level. Then you get desired API and there is no need to pollute the lib's API. Although if you think that your use case is quite common and popular, you can put your suggestions and we can think about them together.

@hanshsieh
Copy link
Author

Thank you. That looks better to me. I was considering whether to propose this library to my team, so I wanted to see what this library could and could not do. I think this is one of the common use cases. Another use case is capturing the arguments used and then grabbing the argument values later for verification. It is suitable for complex object arguments where the matcher isn't convenient.
However, you may want to keep this library simple and focus on specific cases, so it's up to you. Thank you again.

@Serg046 Serg046 reopened this Feb 28, 2023
@Serg046
Copy link
Owner

Serg046 commented Feb 28, 2023

Could you please tell more on the second use case? Isn't it already supported?

var fake = new Fake<Sut>();
var list = new List<int> { 0 };
Console.WriteLine(string.Join(", ", list));
var sut = fake.Rewrite(() => Sut.MutateArgument(list));
sut.Execute();
Console.WriteLine(string.Join(", ", list));

public class Sut
{
    public static void MutateArgument(List<int> list) => list.Add(1);
}
0
0, 1

https://dotnetfiddle.net/Ewhv4o

P.S.

I wanted to see what this library could and could not do

Yes, that's my big bad that there is no documentation. Hope to fix that this year...

@Serg046
Copy link
Owner

Serg046 commented Feb 28, 2023

Aah, guess you meant actual arguments used in replacements, etc. They actually used to be accessible by public API some time ago. Let me think of this once again and probably expose them back. If so, it will be object[][] under fake's instance or something like that.

@hanshsieh
Copy link
Author

hanshsieh commented Feb 28, 2023

Aah, guess you meant actual arguments used in replacements, etc. They actually used to be accessible by public API some time ago. Let me think of this once again and probably expose them back. If so, it will be object[][] under fake's instance or something like that.

Yes. For clarity, it is something like below:

using AutoFake;

namespace Test
{
    public class Order
    {
        public IList<LineItem> Items { get; }
        public Order()
        {
            Items = new List<LineItem>();
        }
    }
    public class LineItem
    {
        public string ProductName;
        public int Price;
        public LineItem(string productName, int price)
        {
            ProductName = productName;
            Price = price;
        }
    }
    public class OrderService
    {
        public string CreateOrder(Order order)
        {
            return "100";
        }
    }

    public class RequestHandler
    {
        private OrderService _orderService = new OrderService();
        public string HandleRequest()
        {
            Order order = new Order();
            order.Items.Add(new LineItem("product 1", 100));
            order.Items.Add(new LineItem("product 2", 200));
            return _orderService.CreateOrder(order);
        }
    }
    
    public class Tests
    {
        [Test]
        public void TestHandleRequest()
        {
            var fake = new Fake<RequestHandler>();
        
            var sut = fake.Rewrite(f => f.HandleRequest());
            sut.Replace((OrderService orderService) => orderService.CreateOrder(Arg.IsAny<Order>()));
            sut.Execute();
            // Capture the recorded "order" arguments of the calls to "OrderService.CreateOrder"
            List<Order> capturedOrders = sut.Captured(() => orderService.CreateOrder(Capture.IsAny<Order>()));
            Assert.That(capturedOrders.Count, Is.EqualTo(1));
            Assert.That(capturedOrders.First().Items.Count, Is.EqualTo(2));
            Assert.That(capturedOrders.First().Items[0].ProductName, Is.EqualTo("product 1"));
        }

        [Test]
        public void TestHandleRequest2()
        {
            var fake = new Fake<RequestHandler>();

            var sut = fake.Rewrite(f => f.HandleRequest());
            Func<Order, bool> checkOrder = (order) => {
                Assert.That(order.Items.Count, Is.EqualTo(2));
                Assert.That(order.Items[0].ProductName, Is.EqualTo("product 1"));
                return true;
            };
            sut.Replace((OrderService orderService) => orderService.CreateOrder(Arg.Is(checkOrder))).Return("1000");
            sut.Execute();
        }
    }
}

The method TestHandleRequest uses an non-existing method Captured to describe the argument we want to capture. Because the CreateOrder might have been called several times, the return value is a List<Order>. I referenced the way that mockito is designed.
But as you can see, it's also achievable with a matcher as shown in TestHandleRequest2.
Therefore, it's just for convenience, not functional limitation.

@Serg046
Copy link
Owner

Serg046 commented Feb 28, 2023

This is achievable and I'd say convenient enough because you can always write extensions but this might stop working if HandleRequest has two CreateOrder calls. I will think of a more simple/universe Captured analog.

@Serg046
Copy link
Owner

Serg046 commented Jul 3, 2023

Unfortunately, I still don't see something better than exposing object[][] Arguments property on sut which is worse than existing API because of types. So I tend to keep API as is if there are no other points supporting the request.

@hanshsieh
Copy link
Author

Unfortunately, I still don't see something better than exposing object[][] Arguments property on sut which is worse than existing API because of types. So I tend to keep API as is if there are no other points supporting the request.

I see, thanks. Please feel free to close this issue.

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

No branches or pull requests

2 participants