My specs are simple - I am trying to create a Dependency Injection Container that does absolutely very little, not as powerful as Unity or any other containers. I dont plan to use it anywhere - just am trying to create one for fun. So the specifications are simple (for now).
Mapping Interface to Type, MappingInterfaceToType » should allow creating instance of type registered In case of multiple constructors, pick the one with injection attribute, MultipleConstructorScenario » should create instance whose Value property is 20 Container should support singleton instances, SingletonScenario » should return the same instance for every invocation Inner container should be supported, SupportForInnerContainer » should return registration from parent and itself
Simple right? Those are the output for my specifications. It it still under works but the output itself explains a lot - thats the power of the MSpecs. Business Analysts write requirements, gives specifications - This can be changed - they give us the requirements and we "program" the specifications. I know it appears to be some rewired Unit Testing, but if unit tests can give me a bunch of specifications and tell me what failed, I would be more than happy to use that.
Now lets write a specification from scratch. Look at the following simple C# code.
public class Singleton { } public class Transient { public Singleton SingletonInstance { get; private set; } public Transient(Singleton ins) { SingletonInstance = ins; } }
I would like to register the Singleton class as - singleton and the transient class as - transient. So my specification is, instances of transient whose dependency is a singleton should get the same instance. So that becomes my "Subject"
[Subject("instances of transient whose dependency is a singleton should get the same instance.")] public class InjectionOfSingletonForTransientResolution { }
Now to verify that specification, I need to "establish a context" which is that I need my container to be ready for use.
Establish context = () => { _builder = new TypeBuilder(); };
Now that context is established, I will just write my specification. I create two instances and both my instances should have the same instance of Singleton.
It Should_Use_The_Same_Instance_when_creating_dependent_Components = () => { var instance1 = _builder.Resolve(); var instance2 = _builder.Resolve (); instance1.ShouldNotEqual(instance2); instance1.SingletonInstance.ShouldNotBeNull(); instance2.SingletonInstance.ShouldNotBeNull(); instance1.SingletonInstance.ShouldEqual(instance2.SingletonInstance); };
But you know this would not happen just like that. I have my context and I know what it should do. I need to tell it why it should do that - that specification would pass "because I am registering dependency as singleton and instance as transient". So I add my reasons on why (or when) the specification would pass.
Because I_Am_Making_A_Dependency_Registration_As_Singleton_And_TestSubject_As_Transient = () => { _builder.Register(new Singleton()); _builder.Register (); };
The whole specification would look like shown below.
[Subject("instances of transient whose dependency is a singleton should get the same instance.")] public class InjectionOfSingletonForTransientResolution { static ITypeBuilder _builder; public class Singleton { } public class Transient { public Singleton SingletonInstance { get; private set; } public Transient(Singleton ins) { SingletonInstance = ins; } } Establish context = () => { _builder = new TypeBuilder(); }; Because I_Am_Making_A_Dependency_Registration_As_Singleton_And_TestSubject_As_Transient = () => { _builder.Register(new Singleton()); _builder.Register (); }; It Should_Use_The_Same_Instance_when_creating_dependent_Components = () => { var instance1 = _builder.Resolve (); var instance2 = _builder.Resolve (); instance1.ShouldNotEqual(instance2); instance1.SingletonInstance.ShouldNotBeNull(); instance2.SingletonInstance.ShouldNotBeNull(); instance1.SingletonInstance.ShouldEqual(instance2.SingletonInstance); }; }
When I first run this my specification failed because my container does not handle that yet.
instances of transient whose dependency is a singleton should get the same instance., InjectionOfSingletonForTransientResolution » Should Use The Same Instance when creating dependent Components (FAIL) Machine.Specifications.SpecificationException: Should be [not null] but is [null] at Machine.Specifications.ShouldExtensionMethods.ShouldNotBeNull(Object anObject) in d:\BuildAgent-03\work\38fe83de684fd902\Source\Machine.Specifications\ExtensionMethods.cs:line 181 at Analytics.Specifications.Container.InjectionOfSingletonForTransientResolution.<.ctor>b__2() in C:\Users\bhargav\documents\visual studio 2010\Projects\Analytics\Analytics.Specifications\Container\TypeBuilderSpecs.cs:line 159 at Machine.Specifications.Model.Specification.InvokeSpecificationField() in d:\BuildAgent-03\work\38fe83de684fd902\Source\Machine.Specifications\Model\Specification.cs:line 75 at Machine.Specifications.Model.Specification.Verify() in d:\BuildAgent-03\work\38fe83de684fd902\Source\Machine.Specifications\Model\Specification.cs:line 53
I fix my container and now I see my specification pass.
One thing to remember is what ever you are working off - your context - it should be static - otherwise the compilation would fail. when looking at others examples, I had a similar question - so here I am telling you upfront.
Running specifications without leaving Visual Studio - No test runners are required.
All my specifications are in a separate class library. I would like to run the specifications without leaving Visual Studio (btw, I am using Express). I remember once upon a time I could make a class library as a startup project and somehow linked NUnit gui runner with the project. I could not get it done with VC# 2010 Express anymore. May be I am missing something. Anyway the solution was to add the following Post-Build event
Once my specifications project it built successfully, it automatically generates a nice output. You can use all command line arguments that mspec supports, for now I only care if they pass or fail, hence the simple one.
Debugging my specifications. It was painful
I tried all different strategies like using System.Diagnostics.Debugger.Break() and what not. ConsoleRunner that comes with mspec (mspec.exe) was crashing complaining that it encountered an user defined breakpoint in the code - yeah that was my intention. Anyway to work around that, I create a console program and in the console application, I added reference to my specifications library and from the Git Hub source code for the mspec.exe (thanks to OSS) the following C# code helped me overcome my limitation of not being able to use R# or TestDriven.Net.
class ContainerSpecsRunner { static void Main(string[] args) { //Console.WriteLine(typeof(ITest).Assembly.Location); Program prog = new Program(new DefaultConsole()); prog.Run(new[] { typeof(ITest).Assembly.Location }); } }
ITest is a type that was defined in my specifications assembly. That way I need not worry about any arguments or hardcode path of my assembly. By the way, at work you might be killed for doing this, I am just doing it at my personal projects.
Some samples?
Ok the biggest problem I had was that I could not find some real world examples, the GIT HUB structure of the project was confusing as hell. I was hoping to find some examples, but I could not. As much as I like the project, I hate to see so little or almost non-existent guidance for new users. So here are some of my specifications. These are written by me - I am just learning the style of specs so forgive me if they are not what you wanted them to be. I am just trying to help.
public interface ITest{ } public class Test: ITest{ [Injector] public Test() { } } [Subject("Mapping Interface to Type")] public class MappingInterfaceToType { static ITypeBuilder _builder; Establish context = () => { _builder = new TypeBuilder(); }; Because of = () => { _builder.Register(); }; It should_allow_creating_instance_of_type_registered = () => { var resolvedObject = _builder.Resolve (); resolvedObject.ShouldNotBeNull(); typeof(Test).ShouldEqual(resolvedObject.GetType()); }; } [Subject("In case of multiple constructors, pick the one with injection attribute")] public class MultipleConstructorScenario { public class Test2 : ITest { public int Value { get; private set; } public Test2(ITest demo) { Value = 10; } [Injector] public Test2() { Value = 20; } } static ITypeBuilder _builder; Establish context = () => { _builder = new TypeBuilder(); }; Because of = () => { _builder.Register (); }; It should_create_instance_whose_Value_property_is_20 = ()=> { var instance = _builder.Resolve (); instance.ShouldBeOfType (); (instance as Test2).Value.ShouldEqual(20); }; } [Subject("Container should support singleton instances")] public class SingletonScenario { static ITypeBuilder _builder; Establish context = () => { _builder = new TypeBuilder(); }; Because instance_Is_Registered = () => { _builder.Register (()=>new Test()); }; It should_return_the_same_instance_for_every_invocation = () => { _builder.Resolve ().ShouldEqual(_builder.Resolve ()); }; } [Subject("Inner container should be supported")] public class SupportForInnerContainer { static ITypeBuilder _builder; static ITypeBuilder _child; Establish context = () => { _builder = new TypeBuilder(); _child = _builder.CreateChildBuilder(); }; Because instance_is_registered_with_child_container = () => { _builder.Register (new int[] { 0, 1, 2 }); _child.Register (); }; It should_return_registration_from_parent_and_itself = () => { var arr = (int[])_child.Resolve (); arr.SequenceEqual(new[] { 0, 1, 2 }).ShouldBeTrue(); _child.Resolve ().ShouldBeOfType (); }; }
Would be nice if it can print my "Because" field names
Just like the console runner prints my "It" fields, it would be nicer if my reasons are printed. I will see if i can do that myself to the project and may contribute a little.
I hope the examples are useful. By no means they are perfect but they can get you started. You can see in the example I detailed in the beginning, I had a whole bunch of Should statements = that is plain wrong. Each specification should define one thing - otherwise it would be a big mess. Please look at this great project and I really am in love with MSpec.
3 comments:
For debugging your specs, I recommend using TestDriven.Net or Resharper's built in MSpec runners. These allow you to right click on an individual test and "Run with Debugger".
@Andy - I know about the runners. I thought it was pretty good but I play on my laptop most of the times and i dont have a VS 2010 setup. I use C# Express edition and you know they dont support addins.
Thanks for sharing this informative information. I love your blog! You will be in our prayers and thoughts!
Post a Comment