3

I'm trying to mock an abstract class from a library that I'm using. I don't have access to the source code, only the decompiled version:

public abstract class Event : IEnumerable<Message>, IEnumerable
{
    protected Event();
    public abstract bool IsValid { get; }
    public IEnumerator<Message> GetEnumerator();
    public IEnumerable<Message> GetMessages();        
}

This decompiled code confuses me slighty. First, the redundant inheritance, and also there's no implementation of non-abstract methods e.g. GetEnumerator or IEnumerable.GetEnumerator(). But it has compiled, and it works so I suppose it's just an artefact of the decompilation (if that is even a thing?)

I have tried the following mock, which compiles and runs without throwing exceptions.

public static Event GetMockedEvent()
{
    var mock = new Mock<Event>();
    mock.Setup(e => e.IsValid).Returns(true);
    mock.As<IEnumerable>().Setup(e => e.GetEnumerator()).Returns(MessageList());

    return mock.Object;
}

private static IEnumerator<Message> MessageList()
{
    yield return GetMockedMessage();
    yield return GetMockedMessage();
}

private static Message GetMockedMessage()
{
    var mock = new Mock<Message>();
    // Unimportant setups...        
    return mock.Object;
}

However I don't get any elements in the mocked object which I test in the following way

var ev = GetMockedEvent();
foreach (var msg in ev)
{
    //
}

But the enumeration is empty, and I cannot figure out why. I have scratched my head with this issue for a full day now so I'd be very appreciative of your help.

Kind regards

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
DoubleTrouble
  • 902
  • 2
  • 7
  • 19
  • `only the decompiled version:` that's not a decompiled version, that's just the public API of that type that needs to be accessible for anyone using it. – Servy Jan 25 '18 at 16:22

3 Answers3

3

When you foreach over a sequence, IIRC the compiler will desugar that to a call to the generic version of GetEnumerator, so that's the one you'll have to mock.

Something like this might do the trick, although I haven't tried:

public static Event GetMockedEvent()
{
    var mock = new Mock<Event>();
    mock.Setup(e => e.IsValid).Returns(true);
    mock.As<IEnumerable<Message>>()
        .Setup(e => e.GetEnumerator())
        .Returns(() => MessageList());

    return mock.Object;
}
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • I added a return function to allow multiple calls. – Nkosi Jan 25 '18 at 20:19
  • At first also I recalled what you said, but in fact the compiler will first look for a public suitable `GetEnumerator` method, and only if not found it searches for the `IEnumerable` interface. This is not much intuitive IMHO, but so [the spec](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#the-foreach-statement) says. – FstTesla Jan 26 '18 at 08:41
  • Thanks for the reply, but I don't get it. Which method exactly is mocked here? With this code `var evts = mock.Object` gets 2 elements but it's not possible to iterate it using `foreach (var msg in evts)` – DoubleTrouble Jan 26 '18 at 09:35
  • @FstTesla Either I'm reading that spec wrong, or the C# compiler doesn't follow the spec. Consider this example that shows that `foreach` works even though the non-generic implementation throws: https://gist.github.com/ploeh/a96c519cb4ec83ab96487b37c84e1e32 – Mark Seemann Jan 26 '18 at 09:42
  • @MarkSeemann I think the first one :) The example you created behaves exactly as I read the spec, since the public implicit implementation of `IEnumerable` "wins" over the non-generic one. Not by chance, when you force the cast to non-generic, the public `GetEnumerator` is not selected. – FstTesla Jan 26 '18 at 10:01
  • @FstTesla I think I misunderstood what you first wrote, but I think I get it now. There's no `GetEnumerator()` method (I misremembered), but there's a version of `GetEnumerator()` that returns `IEnumerator`, and one that returns `IEnumerator` - a sort of return-type polymorphism... – Mark Seemann Jan 26 '18 at 10:08
  • @DoubleTrouble Looking at `Event` a second time, if it truly looks like in the OP, `GetEnumerator` is a non-virtual method, which means that you can't override it. See https://stackoverflow.com/a/1973482/126014 for more details. – Mark Seemann Jan 26 '18 at 13:27
2

Event has three public members (and one explicitly implemented interface method). You've mocked two of them, and then written test code that uses the third. You need to actually mock the GetEnumerator implementation if you want it to return something in your test code (and of course you should mock the non-generic version as well, in case some other test code tries to use it).

Servy
  • 202,030
  • 26
  • 332
  • 449
2

Foreword The code of the Event class that you pasted is only a metadata representation. If you really want to see its source code, use a full decompiler such as ILSpy (VS extension). End foreword

The Event class as-is can't be fully mocked, because the GetEnumerator is not virtual (at least as far as your snippet is telling), so

  • you must use its body as-is; and
  • not even a mocking libraries can replace it.

Since the class implements IEnumerable<Message> implicitly, the foreach loop calls directly the declared method, not the "explicit implementation" that you setup in the GetMockedEvent method.

To be clear, below is the full snippet that I tried to run. I decided to throw a NotImplementedException as a "neutral" replacement for the unknown method bodies.

void Main()
{
    var ev = GetMockedEvent();
    foreach (var msg in ev)
    {
        Console.WriteLine(msg);
    }
}

public abstract class Event : IEnumerable<Message>, IEnumerable
{
    protected Event() { }
    public abstract bool IsValid { get; }
    public IEnumerator<Message> GetEnumerator() { throw new NotImplementedException(); }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    public IEnumerable<Message> GetMessages() { throw new NotImplementedException(); }     
}

public class Message { }

public static Event GetMockedEvent()
{
    var mock = new Mock<Event>();
    mock.Setup(e => e.IsValid).Returns(true);
    mock.As<IEnumerable<Message>>().Setup(e => e.GetEnumerator()).Returns(MessageList());
    // The next line doesn't work either because the method is not virtual
    //mock.Setup(e => e.GetEnumerator()).Returns(MessageList());

    return mock.Object;
}

private static IEnumerator<Message> MessageList()
{
    yield return GetMockedMessage();
    yield return GetMockedMessage();
}

private static Message GetMockedMessage()
{
    var mock = new Mock<Message>();
    // Unimportant setups...
    return mock.Object;
}
FstTesla
  • 844
  • 1
  • 7
  • 10
  • Thanks for the clarification, so the metadata representation are like c++ header files? I suppose then there is nothing I can mock to make this work as I want? – DoubleTrouble Jan 26 '18 at 09:45
  • 1
    Those metadata have been extracted by Visual Studio (correct me if I'm wrong) and represent the signatures of the public methods of the class, with very few details and, of course, no method bodies. Judging from the metadata, you can't mock this class in a useful way because it doesn't look designed to be extensible (no virtual `GetEnumerator`) nor to be mocked for testing (for example providing a header interface). – FstTesla Jan 26 '18 at 10:10
  • 1
    A solution could be creating your own subclass of `Event` for the purpose of testing: after all, that's an abstract class. Maybe the metadata representation is hiding something useful that you may discover when implementing the subclass. – FstTesla Jan 26 '18 at 10:15
  • Hi, I tried that but there are no virtual methods I can override. I used ILSpy to inspect the code and `IEnumerable.GetEnumerator()`, `GetEnumerator()`, `GetEnumerable()`, `GetMessages()` all have non-virtual implementations in Event. In the end they all use an abstract method member `iterator()` however it's an internal member so I cannot override it. Here is the decomplied code: https://hastebin.com/raw/luguruhomi if you care to have a look! – DoubleTrouble Jan 26 '18 at 10:27
  • 1
    I see. The decompiled code confirms my first opinion about this class being neither externally extensible nor mockable. – FstTesla Jan 26 '18 at 10:48
  • Thanks for your input, much appreciated! – DoubleTrouble Jan 26 '18 at 10:50