BDD with Specflow, Moq and Microsoft Unit Testing framework
BDD (Behavior Driven Development) is very popular in these days. I know a lot of companies used BDD in our projects. So I also had some curios about it and I applied for my current project. I used Specflow, Moq and Microsoft Testing framework for working. I did it and found out some things very interested in. In this post I will not focus on what it BDD? What is Mock, stub or how to write a unit testing? Readers can easy find on internet, just googling it and everything will be okay. I just focus on the some best things I found when I implemented the BDD on my code. I used AAA (Arrange-Act-Assert) and GWT (Given-When-Then) patterns in my code. They are clearly and easily to understand and become standard when you write unit testing and mocking.
Now we discuss about a problem when we write unit testing and how to get rid of it. So what's problem? When we write unit testing and try to arrange a lot of things to testing. And the code will be messy and a line of code will become unmanageable, so it is hard to maintain at will. I will give you a simple sample. Assuming we have a small application for get all articles from database. To keep it simple I will not use ORM for communicate and mapping object between Object Model and Database. So the article will have a lot of information to populating. Some of them are Category, Article Information, Author Information, Article content, Article Media Item, Keyword, Topic and Sub topics. And we need to populate all of this, look like a complicate scenario.
We try to make it work. And it has many repositories inside the article manager class. So that mean your mock objects will equal with number of repositories in article manager class. What's happen with your test class? You will declare an object for mock and in its constructor; you will mock it and set it to parameter constructor for article manager class. Like this
var dummyRepositoryMock1 = new Mock<IDummyRepository1>();
var dummyRepositoryMock2 = new Mock<IDummyRepository2>();
var dummyRepositoryMock3 = new Mock<IDummyRepository3>();
var dummyRepositoryMock4 = new Mock<IDummyRepository4>();
var dummyRepositoryMock5 = new Mock<IDummyRepository5>();
var dummyRepositoryMock6 = new Mock<IDummyRepository6>();
// prepare the stub objects for return
var retObject1 = ...;
var retObject2 = ...;
var retObject3 = ...;
var retObject4 = ...;
var retObject5 = ...;
var retObject6 = ...;
dummyRepository1.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects1);
dummyRepository2.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects2);
dummyRepository3.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects3);
dummyRepository4.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects4);
dummyRepository5.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects5);
dummyRepository6.Setup(framework=>framework.SomeMethodCall(It.IsAny<int>())).Returns(retObjects6);
and the constructor of Article manager class will be
var articleManager = new ArticleManager(
dummyRepositoryMock1.Object,
dummyRepositoryMock2.Object,
dummyRepositoryMock3.Object,
dummyRepositoryMock4.Object,
dummyRepositoryMock5.Object,
dummyRepositoryMock6.Object);
So how can we optimize a line of code for this processing? I write the abstract class like this
public abstract class AutoMockObject
{
private readonly IDictionary<Type, dynamic> _mocks;
protected AutoMockObject()
{
_mocks = new Dictionary<Type, dynamic>();
}
public virtual void Register<TObject>(Action<Mock<TObject>> action) where TObject : class
{
var mockObject = new Mock<TObject>();
_mocks.Add(typeof(TObject), mockObject);
action(mockObject);
}
public virtual TObject Resolve<TObject>() where TObject : class
{
dynamic mock;
return !_mocks.TryGetValue(typeof(TObject), out mock) ? null : mock.Object;
}
}
Make sure you test class should be inherited from AutoMockObject. And finally I just mock it with a bit effort as below
Register<IArticleRepository>(mock =>
{
mock.Setup(framework => framework.GetAll()).Returns(GetAllArticleStub.ArticleList);
mock.Setup(framework => framework.GetKeywordsByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.KeywordList);
mock.Setup(framework => framework.GetTopicsByArticleId(It.IsAny<int>(), false)).Returns(GetAllArticleStub.TopicList);
mock.Setup(framework => framework.GetTopicsByArticleId(It.IsAny<int>(), true)).Returns(GetAllArticleStub.TopicList);
});
Register<IArticleCategoryRepository>(mock => mock.Setup(framework => framework.GetById(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleCategory));
Register<IArticleInfoRepository>(mock => mock.Setup(framework => framework.GetArticleInformationById(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleInformation));
Register<IAuthorInfoRepository>(mock => mock.Setup(framework => framework.GetAuthorInformationById(It.IsAny<int>())).Returns(GetAllArticleStub.AuthorInformation));
Register<IArticleContentRepository>(mock => mock.Setup(framework => framework.GetSummaryByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleContentList));
Register<IArticleMediaItemRepository>(mock =>
{
mock.Setup(framework => framework.GetArticleMediaItemByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleMediaItemList);
mock.Setup(framework => framework.GetArticleMediaItemAltTextByArticleMediaItemId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleMediaItemAltTextList);
});
_articleManager = new ArticleManager(
Resolve<IArticleRepository>(),
Resolve<IArticleCategoryRepository>(),
Resolve<IArticleContentRepository>(),
Resolve<IReferenceRepository>(),
Resolve<IArticleInfoRepository>(),
Resolve<IAuthorInfoRepository>(),
Resolve<IPublishingArticleRepository>(),
Resolve<IArticleMediaItemRepository>()
);
Look like simple and elegant, isn't it? Just a little code for get rid of messy and redundant code.
Now I will show you how can I make all solutions working.
The first thing I will write a feature like this
Feature: Get all articles from Repository
In order to get all articles
As a cms user
So that I can get and process in all articles
@GetAllArticles
Scenario: Get All articles
Given I don't input any parametters
When I call the GetAllArticle function from service
Then the result should be a list of article
@GetAllArticlesWithError
Scenario: Get All articles with error
Given I dont input any parametters
When I call the GetAllArticle function from service
Then the result should be a exception
Add App.config and add some line of code as below:
<configSections>
<section
name="specFlow"
type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow"/>
</configSections>
<specFlow>
<unitTestProvider name="MsTest" />
</specFlow>
And after that Specflow will generate the code for Microsoft Testing framework (default it will generate for NUnit testing) as below
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by SpecFlow (http://www.specflow.org/).
// SpecFlow Version:1.7.0.0
// SpecFlow Generator Version:1.7.0.0
// Runtime Version:4.0.30319.431
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
#region Designer generated code
namespace xxx.xxx.xxx
{
using TechTalk.SpecFlow;
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "1.7.0.0")]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute()]
public partial class GetAllArticlesFromRepositoryFeature
{
private static TechTalk.SpecFlow.ITestRunner testRunner;
#line 1 "GetAllArticles.feature"
#line hidden
[Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute()]
public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)
{
testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Get all articles from Repository", "In order to get all articles\r\nAs a cms user\r\nSo that I can get and process in all" +
" articles", ProgrammingLanguage.CSharp, ((string[])(null)));
testRunner.OnFeatureStart(featureInfo);
}
[Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupAttribute()]
public static void FeatureTearDown()
{
testRunner.OnFeatureEnd();
testRunner = null;
}
[Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute()]
public virtual void TestInitialize()
{
if (((TechTalk.SpecFlow.FeatureContext.Current != null)
&& (TechTalk.SpecFlow.FeatureContext.Current.FeatureInfo.Title != "Get all articles from Repository")))
{
xxx.xxx.xxx.GetAllArticlesFromRepositoryFeature.FeatureSetup(null);
}
}
[Microsoft.VisualStudio.TestTools.UnitTesting.TestCleanupAttribute()]
public virtual void ScenarioTearDown()
{
testRunner.OnScenarioEnd();
}
public virtual void ScenarioSetup(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
{
testRunner.OnScenarioStart(scenarioInfo);
}
public virtual void ScenarioCleanup()
{
testRunner.CollectScenarioErrors();
}
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
[Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Get All articles")]
[Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute("FeatureTitle", "Get all articles from Repository")]
public virtual void GetAllArticles()
{
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Get All articles", new string[] {
"GetAllArticles"});
#line 7
this.ScenarioSetup(scenarioInfo);
#line 8
testRunner.Given("I don\'t input any parametters");
#line 9
testRunner.When("I call the GetAllArticle function from service");
#line 10
testRunner.Then("the result should be a list of article");
#line hidden
this.ScenarioCleanup();
}
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
[Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Get All articles with error")]
[Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute("FeatureTitle", "Get all articles from Repository")]
public virtual void GetAllArticlesWithError()
{
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Get All articles with error", new string[] {
"GetAllArticlesWithError"});
#line 13
this.ScenarioSetup(scenarioInfo);
#line 14
testRunner.Given("I dont input any parametters");
#line 15
testRunner.When("I call the GetAllArticle function from service");
#line 16
testRunner.Then("the result should be a exception");
#line hidden
this.ScenarioCleanup();
}
}
}
#endregion
Look like terrible and hard to understand, itsn't it? No problem, don't care about it. It will be fine Don't scare it :)
The first thing, I try to write some interface to make a skeleton for solution
public interface IArticleRepository : IRepositoryEntityBase<Article>
{
...
}
public interface IArticleCategoryRepository : IRepositoryEntityBase<ArticleCategory>
{
}
public interface IArticleInfoRepository : IRepositoryEntityBase<ArticleInfo>
{
...
}
public interface IArticleContentRepository : IRepositoryEntityBase<ArticleContent>
{
....}
public interface IArticleMediaItemRepository : IRepositoryEntityBase<MediaItem>
{
}
and an interface for article manager
public interface IArticleManager
{
}
Certainly, we should have article manager instance class
public class ArticleManager : IArticleManager
{
private readonly IArticleContentRepository _articleContentRepository;
private readonly IArticleInfoRepository _articleInfoRepository;
private readonly IArticleRepository _articleRepository;
private readonly IAuthorInfoRepository _authorInfoRepository;
private readonly IArticleCategoryRepository _categoryRepository;
private readonly IPublishingArticleRepository _publishingArticleRepository;
private readonly IReferenceRepository _referenceRepository;
private readonly IArticleMediaItemRepository _articleMediaItemRepository;
public ArticleManager(IArticleRepository articleRepository
, IArticleCategoryRepository categoryRepository
, IArticleContentRepository articleContentRepository
, IReferenceRepository referenceRepository
, IArticleInfoRepository articleInfoRepository
, IAuthorInfoRepository authorInfoRepository
, IPublishingArticleRepository publishingArticleRepository
, IArticleMediaItemRepository articleMediaItemRepository)
{
_articleRepository = articleRepository;
_authorInfoRepository = authorInfoRepository;
_articleInfoRepository = articleInfoRepository;
_referenceRepository = referenceRepository;
_articleContentRepository = articleContentRepository;
_categoryRepository = categoryRepository;
_publishingArticleRepository = publishingArticleRepository;
_articleMediaItemRepository = articleMediaItemRepository;
}
....................
}
Note: I use Unity for dependency injection framework, but I will not mention it this post, because it is very easy to achive.
After that I add 2 step definitions for get all article and get all article with an exception should be throw out
[Binding]
public class GetAllArticleSteps : AutoMockObject
{
private IArticleManager _articleManager;
private IEnumerable<Article> _result;
[Given(@"I don't input any parametters")]
[StepScope(Tag = "GetAllArticles")]
public void GivenIDontInputAnyParamatters()
{
Register<IArticleRepository>(mock =>
{
mock.Setup(framework => framework.GetAll()).Returns(GetAllArticleStub.ArticleList);
mock.Setup(framework => framework.GetKeywordsByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.KeywordList);
mock.Setup(framework => framework.GetTopicsByArticleId(It.IsAny<int>(), false)).Returns(GetAllArticleStub.TopicList);
mock.Setup(framework => framework.GetTopicsByArticleId(It.IsAny<int>(), true)).Returns(GetAllArticleStub.TopicList);
});
Register<IArticleCategoryRepository>(mock => mock.Setup(framework => framework.GetById(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleCategory));
Register<IArticleInfoRepository>(mock => mock.Setup(framework => framework.GetArticleInformationById(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleInformation));
Register<IAuthorInfoRepository>(mock => mock.Setup(framework => framework.GetAuthorInformationById(It.IsAny<int>())).Returns(GetAllArticleStub.AuthorInformation));
Register<IArticleContentRepository>(mock => mock.Setup(framework => framework.GetSummaryByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleContentList));
Register<IArticleMediaItemRepository>(mock =>
{
mock.Setup(framework => framework.GetArticleMediaItemByArticleId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleMediaItemList);
mock.Setup(framework => framework.GetArticleMediaItemAltTextByArticleMediaItemId(It.IsAny<int>())).Returns(GetAllArticleStub.ArticleMediaItemAltTextList);
});
_articleManager = new ArticleManager(
Resolve<IArticleRepository>(),
Resolve<IArticleCategoryRepository>(),
Resolve<IArticleContentRepository>(),
Resolve<IReferenceRepository>(),
Resolve<IArticleInfoRepository>(),
Resolve<IAuthorInfoRepository>(),
Resolve<IPublishingArticleRepository>(),
Resolve<IArticleMediaItemRepository>()
);
}
[When(@"I call the GetAllArticle function from service")]
[StepScope(Tag = "GetAllArticles")]
public void WhenICallTheGetAllArticleFunctionFromTheService()
{
_result = _articleManager.GetAllArticle();
}
[Then(@"the result should be a list of article")]
[StepScope(Tag = "GetAllArticles")]
public void ThenTheResultShouldBe()
{
Assert.IsNotNull(_result);
Assert.IsTrue(_result.Count() == 1);
}
}
[Binding]
public class GetAllArticleWithErrorSteps
{
private IArticleManager _articleManager;
private IEnumerable<Article> _result;
[Given(@"I dont input any parametters")]
[StepScope(Tag = "GetAllArticlesWithError")]
public void GivenIDontInputAnyParamettersWithError()
{
_articleManager = new ArticleManager(null, null, null, null, null, null, null, null);
}
[When(@"I call the GetAllArticle function from service")]
[StepScope(Tag = "GetAllArticlesWithError")]
public void WhenICallTheGetAllArticleFunctionFromServiceWithError()
{
try
{
_result = _articleManager.GetAllArticle();
}
catch (Exception e)
{
ScenarioContext.Current.Add("NullException_GetAllArticleMethod", e);
}
}
[Then("the result should be a exception")]
[StepScope(Tag = "GetAllArticlesWithError")]
public void ThenTheResultShouldBe()
{
Assert.IsTrue(ScenarioContext.Current.ContainsKey("NullException_GetAllArticleMethod"));
}
}
This is just a simple scenario for demo. Nothing is complex here.
Now try to run all unit testings inside Visual Studio 2010, it will be can run because you don't have an implementation for article business class. Now we add some code for it.
public IEnumerable<Article> GetAllArticle()
{
IEnumerable<Article> articles = _articleRepository.GetAll();
foreach (var article in articles)
{
SetReferenceData(article);
}
return articles;
}
private void SetReferenceData(Article article)
{
Guard.ArgumentNotNull(article, "article");
article.Category = _categoryRepository.GetById(article.CategoryId);
article.ArticleInformation = _articleInfoRepository.GetArticleInformationById(article.ArticleInformationId);
article.AuthorInformation = _authorInfoRepository.GetAuthorInformationById(article.AuthorInformationId);
article.Keywords = _articleRepository.GetKeywordsByArticleId(article.ArticleId).ToList();
article.Topics = _articleRepository.GetTopicsByArticleId(article.ArticleId, false).ToList();
article.SubTopics = _articleRepository.GetTopicsByArticleId(article.ArticleId, true).ToList();
List<ArticleContent> summaryContent = _articleContentRepository.GetSummaryByArticleId(article.ArticleId).ToList();
if (summaryContent.Count > 1)
{
var baseContent = summaryContent.Find(ac => ac.LocaleCode == article.LocaleCode);
summaryContent.Remove(baseContent);
summaryContent.Insert(0, baseContent);
}
article.ArticleContents = summaryContent;
var mediaItems = _articleMediaItemRepository.GetArticleMediaItemByArticleId(article.ArticleId);
foreach (var articleMediaItem in mediaItems)
{
articleMediaItem.ArticleMediaItemAltTexts = _articleMediaItemRepository.GetArticleMediaItemAltTextByArticleMediaItemId(
articleMediaItem.ArticleMediaItemId);
}
article.MediaItems = mediaItems.ToList();
}
Make sure you add all methods for all repositories, try to run test again. And now it pass. How about your thinking? All comment for discussion are welcome. Happy coding and see you next post.