3

I'm trying to run some initialization code before a test. I've tried the suggestions in other questions, but it doesn't seem to work. My domain model raises events via the following class:

public static class DomainEvents
{
    private static readonly object @lock = new object();
    private static Action<IDomainEvent> raiseEvent;

    public static void Raise<TEvent>(TEvent @event) where TEvent : class, IDomainEvent 
    {
         // omitted for brevity
    }

    public static void RegisterEventPublisher(Action<IDomainEvent> eventPublisher)
    {
        lock (@lock)
        {
            raiseEvent = eventPublisher;
        }
    }
}

For testing purposes I would like to capture these events in a static list. What is the best way of doing this?

Update

The problem was caused by the order in which the tests are run (which as Alexander points out below, is not guaranteed). In one of my specs I had registered a mock event publisher. The fact that the spec would often run in different orders meant that a) to begin with I didn't know I had the issue (the "problem" spec always ran last) and b) Once I started having the issue, the number of failing tests would often vary between runs (making it even more confusing).

The lesson learned - clean up any static resources after each context has run. You can do this by implementing ICleanupAfterEveryContextInAssembly.

Community
  • 1
  • 1
Ben Foster
  • 34,340
  • 40
  • 176
  • 285

2 Answers2

4

Maybe I'm misunderstanding the issue, but the basic pattern is:

public class WhenSomeDomainEventIsRaised
{
    private IList<IDomainEvent> EventsRaised = new List<IDomainEvent>();

    Establish context = () => 
    {
        // subscribe to events; when raised, add to EventsRaised list
    }
}

If you want to do this for all tests or a subset of tests:

public abstract class DomainSpecification
{
    protected IList<IDomainEvent> EventsRaised = new List<IDomainEvent>();

    Establish context = () => 
    {
        // subscribe to events; when raised, add to EventsRaised list
    }
}

You can have all specs that need this behaviour inherit from this class, and MSpec will take care of running all Establish blocks along the inheritance hierarchy.

Jay
  • 56,361
  • 10
  • 99
  • 123
2

This works for me:

using System;
using System.Collections.Generic;

using Machine.Specifications;

namespace AssemblyContextSpecs
{
  public static class DomainEvents
  {
    static readonly object @lock = new object();

    static Action<IDomainEvent> raiseEvent;

    public static void Raise<TEvent>(TEvent @event) where TEvent : class, IDomainEvent
    {
      raiseEvent(@event);
    }

    public static void RegisterEventPublisher(Action<IDomainEvent> eventPublisher)
    {
      lock (@lock)
      {
        raiseEvent = eventPublisher;
      }
    }
  }

  public interface IDomainEvent
  {
  }

  class FooEvent : IDomainEvent
  {
  }

  public class DomainEventsContext : IAssemblyContext
  {
    internal static IList<IDomainEvent> Events = new List<IDomainEvent>();

    public void OnAssemblyStart()
    {
      DomainEvents.RegisterEventPublisher(x => Events.Add(x));
    }

    public void OnAssemblyComplete()
    {
    }
  }

  public class When_a_domain_event_is_raised
  {
    Because of = () => DomainEvents.Raise(new FooEvent());

    It should_capture_the_event =
      () => DomainEventsContext.Events.ShouldContain(x => x.GetType() == typeof(FooEvent));
  }
}

Shouldn't RegisterEventPublisher rather be RegisterEventSubscriber?

Alexander Groß
  • 10,200
  • 1
  • 30
  • 33
  • This does work for me on a single specification. The issue I seem to be having is when running multiple specs that fire events. I have 20 or so specs that fire events but I only up with 5 events in the list. Perhaps this is a threading issue? – Ben Foster Apr 19 '12 at 09:52
  • MSpec doesn't create threads by itself. Currently there's only one limitation w.r.t. ReSharper, that is `IAssemblyContext` is run for every context. They normally run once per assembly (TD.Net, console). – Alexander Groß Apr 19 '12 at 10:00
  • Is is possible that this is an ordering issue? MSpec doesn't guarantee the order in which contexts are executed, hence it might be that not "all previous" events are in the list. – Alexander Groß Apr 19 '12 at 10:02
  • I don't necessarily care about the order, providing I can clear the list before each specification is run. The issue does appear to be across multiple contexts. When I remove all but one of the contexts (containing 5 specs that raise events) they are all successful. When I add the the contexts back in, the same 5 fail. – Ben Foster Apr 19 '12 at 10:04
  • What runner do you use? Can you post some code that reproduces the issue? – Alexander Groß Apr 19 '12 at 10:09
  • I eventually found the issue. Thanks for your help, in particular I realized I should be using the MSpec "Should" assertions for the list since the report will then give you the contents (which helped diagnose the issue). Previously I was returning a boolean and asserting that. As a side note, is there a way to run a method *before* every context (the opposite of ICleanupAfterEventContextInAssembly)? – Ben Foster Apr 19 '12 at 10:27
  • Unfortuntately, currently there's no `IRunBeforeEveryContextInAssembly`. We could certainly add something like this, I'm awaiting your pull request :-) Glad you could solve the problems with your specs! – Alexander Groß Apr 19 '12 at 10:33