Lars-Erik's bloghttp://blog.aabech.no/blog/
Ramblings about Umbraco, .net and JavaScript development. With a sprinkle of other stuff.Articulate, blogging built on Umbraco1137http://blog.aabech.no/archive/umbraco-unit-testing-workshop-material/
umbracounit testingUmbraco Unit Testing Workshop Material<p>For this years Umbraco UK Festival I was honored to be invited to run a unit testing workshop. It's a culmination of my talks and workshops until now, and covers everything you need to get going with testing the Umbraco backoffice, controller logic, external calls and finally front-end UI.</p>
<p>I know there's demand for this kind of training, and it might be difficult to get to the conferences where it's happening. For those of you, and those who feel bold and confident, I've released the material under the MIT license.</p>
<p>I can't promise I'll be able to help out if you go at it, but keep an eye out for a conference near you and encourage the organizers to host a workshop. ;)</p>
<p>You can clone the &quot;workshop-start&quot; branch from Bitbucket. The slides and workbook are included at the root of the repo.</p>
<p>https://bitbucket.org/bleedo/umbraco-testing-workshop-h2-2017/src/?at=workshop-start</p>
<p>You'll need</p>
<ul>
<li>node</li>
<li>npm task runner (visual studio extension)</li>
<li>razorgenerator (visual studio extension)</li>
</ul>
<p>Happy testing!</p>
Thu, 09 Nov 2017 10:09:16 Z2017-11-09T10:09:16Z1133http://blog.aabech.no/archive/exploiting-approvaltests-for-clearer-tests/
unit testingExploiting ApprovalTests for clearer tests<h2>What's this for?</h2>
<p>Ever written several asserts in one test because you have a big graph you want to verify? How 'bout files or maybe <a href="/archive/testing-views-with-razorgenerator">razor views</a>? Several asserts often clutter up the tests. Big strings also make the actuall calls hard to see for all that content. Putting big strings in files is a good idea to avoid that, but few people do. It adds another &quot;menial&quot; task when you're in the zone. But what if it was dead easy?</p>
<p>I once maintained a Resharper extension, and their example tests had so called &quot;gold&quot; files that they compared to output. The squigglies was represented by special characters around terms. So they just compared the output of a text renderer to the text in a file. Great idea. One limitation though: with a regular Assert.Equals you just see the segment around the first mismatch. Guess what, there's a tool that's been around for at least 10 years that solves all those issues, and more.</p>
<h2>Approval tests, eh?</h2>
<p>Sounds a bit like acceptance tests, right? Don't be fooled, it's purpose is to serve all the way down to the &quot;unit test layer&quot; of your tests. I've found it to be a huge timesaver, as well as making my tests so much more clear.</p>
<p>I know you're thinking &quot;Shut up and give me an example, then!&quot;, so let's have a look. I've got this unit test from my post about <a href="/archive/testing-views-with-razorgenerator">testing razor views</a> that I never really asserted anything in. I just output the result to console and assert inconclusive. Here it is for reference:</p>
<pre><code>[TestFixture]
public class When_Displaying_An_Event
{
[Test]
public void It_Is_Rendered_With_A_Name_Date_And_A_Link()
{
var view = new _Views_Partials_Event_cshtml();
var actionResult = GetConcertEvent();
Assert.AreEqual(&quot;Event&quot;, actionResult.ViewName);
var renderedResult = view.Render((Event) actionResult.Model);
Console.WriteLine(renderedResult);
Assert.Inconclusive(&quot;Way too big to assert here.&quot;);
}
}
</code></pre>
<p>It outputs the following HTML:</p>
<pre><code>&lt;div&gt;
&lt;a href=&quot;https://eventsite/123&quot;&gt;
&lt;label&gt;
Concert of your life
&lt;/label&gt;
&lt;span&gt;
fredag 31. desember 2049 23.59
&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<h2>Let's see it</h2>
<p>Now let's assert this with ApprovalTests. To use it, you just <code>install-package ApprovalTests</code> with your trusty package manager console. Make sure to install it in your test project. ;)</p>
<p>Now instead of <code>Assert</code>, we ask ApprovalTests to <code>Verify</code> our data instead. It even has a special overload for this concrete case: <code>Approvals.VerifyHtml</code>. So we rewrite the test as such:</p>
<pre><code>[Test]
public void It_Is_Rendered_With_A_Name_Date_And_A_Link()
{
var view = new _Views_Partials_Event_cshtml();
var actionResult = GetConcertEvent();
Assert.AreEqual(&quot;Event&quot;, actionResult.ViewName);
var renderedResult = view.Render((Event) actionResult.Model);
Approvals.VerifyHtml(renderedResult);
}
</code></pre>
<p>Now when we run our test, we get this nice little welcoming message from ApprovalTests:</p>
<p><img src="http://blog.aabech.no/blog/media/1033/welcome-to-approvaltests.png" alt="Exception: Welcome to ApprovalTests" /></p>
<p>It tells us we're missing a vital part: A reporter. It's possible to use <code>DiffReporter</code> to launch your favorite configured difftool. But if you're in Visual Studio, there's a special treat: <code>VisualStudioReporter</code>. Let's add that to our fixture and see what happens:</p>
<pre><code>[UseReporter(typeof(VisualStudioReporter))]
[TestFixture]
public class When_Displaying_An_Event
{
[Test]
public void It_Is_Rendered_With_A_Name_Date_And_A_Link()
{
// ...
}
}
</code></pre>
<p>And we run it:</p>
<p><img src="http://blog.aabech.no/blog/media/1032/first-result.png" alt="ApprovalTests first result have empty approved" /></p>
<p>What hits you is probably the big failure statement there, but look again - up at the top there.
We've got a diff opened, the result on the left hand and a big green field on the right side.<br />
What just happened is that ApprovalTests took our result, stored it in a received file, and at the same time wrote an empty &quot;approved&quot; file. It then proceeded to compare, and finally pop a diff of those two files.<br />
Isn't that just beautiful? Everything that makes your test fail in one clear diagram.</p>
<p>The &quot;procedure to follow&quot; here, is to &quot;approve&quot; results when you're happy. To do that, you just copy and paste. Let's do that now:</p>
<p><img src="http://blog.aabech.no/blog/media/1030/first-approved.png" alt="First approved with invalid indenting" /></p>
<p><em>If you've got ReSharper, it's probably gonna try to format everything nicely when you paste. To have it in the original, ugly indentation state, press Ctrl+Z (undo) once after pasting.</em></p>
<p><em>Regarding the indentation that's off. It seems to be a bug with HTML in the current version of ApprovalTests, so stupid me for choosing this example. I'll update the post if it gets fixed.</em></p>
<p>We can now run our test again, and it's gonna pass. When it passes, it doesn't bother to open the diff.</p>
<p><img src="http://blog.aabech.no/blog/media/1031/first-passing.png" alt="It passes with an approved file" /></p>
<p>This means we're just gonna get diffs for whatever is currently failing. Even if we run our entire suite of tests. Now there's a couple of housekeeping things to keep in mind:</p>
<h2>Commit approvals only</h2>
<p>If you were paying attention, you noticed we got two files adjacent to our test source file. One is named [TestFixtureName].[TestMethodName].received.html and one is named [TestFixtureName].[TestMethodName].approved.html. If you ran a test that passed, you'll actually just have your approved file. <strong>You want those approved files in source control!</strong></p>
<p>The received files though, might end up not being cleaned up for one or the other reason. Hopefully just that you didn't bother to commit a fully passing build. I'm sure you didn't do that to the master branch, though. In any case, make sure to <em>ignore</em> your received files. This pattern in .gitignore generally does the trick:</p>
<p><code>*.received.*</code> </p>
<h2>That's just the tip of the iceberg</h2>
<p>We've seen the <code>VerifyHtml</code> bit. One of my personal favorites is it's sibling <code>VerifyJson</code>. I keep an extension on <code>object</code> in my tests, called <code>.ToJson()</code>. With it, I can just go:</p>
<pre><code>Approvals.VerifyJson(myBigGraph.ToJson());
</code></pre>
<p>The diff is done with prettified JSON, so it's super simple to find the property / area that has changed or doesn't work. Knowing the area of the graph should also make it easier to find the usage that is wrong.</p>
<p>There's a vanilla <code>Verify</code> method too, and it saves plain text files. It's useful in cases where you have nice <code>ToString()</code> implementations. Let's try with a &quot;person&quot;:</p>
<pre><code>public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $&quot;{FirstName} {LastName} ({Age})&quot;;
}
}
[Test]
public void Comparing_Objects()
{
var person = new Person
{
FirstName = &quot;Lars-Erik&quot;,
LastName = &quot;Aabech&quot;,
Age = 19
};
Approvals.Verify(person);
}
</code></pre>
<p>It produces the following .received file:</p>
<pre><code>Lars-Erik Aabech (19)
</code></pre>
<p>It can be approved like that.<br />
We can also do lists with &quot;big brother&quot; <code>VerifyAll</code>:</p>
<pre><code>[Test]
public void Comparing_Lists()
{
var list = new List&lt;Person&gt;
{
new Person {FirstName = &quot;Lars-Erik&quot;, LastName = &quot;Aabech&quot;},
new Person {FirstName = &quot;Dear&quot;, LastName = &quot;Reader&quot;}
};
Approvals.VerifyAll(list, &quot;&quot;);
}
</code></pre>
<p>Which, unsurprisingly outputs:</p>
<pre><code>[0] = Lars-Erik Aabech
[1] = Dear Reader
</code></pre>
<p>Now how bout that?<br />
I don't know how quickly you got hooked, but I certainly find it sneaking into more and more of my tests.</p>
<p>It can even do images, but I'll let <a href="http://jamessouth.me/">James</a> blog about that one.</p>
<p>So what are you lingering around here for? Run over to nuget and get your copy, or lurk around some more at the <a href="http://approvaltests.sourceforge.net/">ApprovalTests</a> project site. There's great examples, even though they might not be in your favorite language.</p>
<p>Happy approving! :)</p>
Mon, 23 Oct 2017 20:14:08 Z2017-10-23T20:14:08Z1127http://blog.aabech.no/archive/umbracosupport-got-typed-content/
unit testingumbracoUmbracoSupport got typed content<h2>What's UmbracoSupport?</h2>
<p><code>UmbracoSupport</code> is a class I've been introducing to my unit tests over the last year or so.
It allows me to have my own hierarchy for tests, as well as re-using all of Umbraco's own
stubbing code. I've written about it in a post called <a href="/archive/the-basics-of-unit-testing-umbraco-just-got-simpler">Unit testing Umbraco just got simpler</a>,
and its gut's code is described in details in <a href="/archive/the-basics-of-unit-testing-umbraco">The basics of unit testing Umbraco</a>.</p>
<h2>A quick primer on what's already available</h2>
<p>The <code>BaseDatabaseFactoryTest</code> in <code>Umbraco.Tests</code> has a method called <code>GetXmlContent</code>.
It replaces the <code>umbraco.config</code> file that acts as the cache at runtime.
It makes <code>UmbracoContext.Current.ContentCache</code> tick in unit tests.
The base tests out of the box has a small flaw though. They can't &quot;popuplate&quot; properties.
All you get is the hierarchy.</p>
<p>Usually I've injected an <code>IPublishedContentCache</code> to my controllers. When testing them,
I've created a mock instance of the <code>IPublishedContentCache</code>. However, all my code has to use
the non-context aware overloads. For instance <code>cache.GetById(umbracoContext, false, id)</code>.
There's also a whole lot of ugly mocking code going on to set up queries and stubbed content.
How to stub properties on stubbed content is described in <a href="/archive/slides-from-cg16-and-testing-ipublishedcontent-properties/">Slides from CG 2016 and testing IPublishedContent properties</a>.</p>
<h2>So what's new?</h2>
<p>As mentioned, I've been throwing around all kinds of ugly stubbing code for content and I've also been tampering with <code>Umbraco.Tests</code>'s <code>GetXmlContent()</code> to use the &quot;built-in&quot; content stubs.
It's all been done before in misc. tests in Umbraco. I finally got my s**t together and refactored all my setup spaghetti into a few small helpers on the <code>UmbracoSupport</code> class.</p>
<p>Let's go over them in increasing &quot;integrationness&quot;.</p>
<h2>Pure hierarchy</h2>
<p>Your basic hierarchy structure can be set up by simply returning a string from an overload of <code>BaseDatabaseFactoryTest.GetXmlContent</code>. <code>UmbracoSupport</code> overloads this method and returns whatever you've set on the <code>UmbracoSupport.ContentCacheXml</code> property. I recommend using the technique described in <a href="/archive/automating-creation-of-source-data-for-tests">Automating creating of source data for tests</a> with this. You can even extend that code to have fixture specific content caches.</p>
<p>In any case, to make this work, you just need to set the XML in the setup method.</p>
<p><em>Note: I've got some probs with the markdown parsing here, imagine the CDATA parts of the XML is correctly written.</em></p>
<pre><code>[SetUp]
public void Setup()
{
umbracoSupport = new UmbracoSupport();
umbracoSupport.SetupUmbraco();
// This XML is what the ContentCache will represent
umbracoSupport.ContentCacheXml = @&quot;
&lt;?xml version=&quot;&quot;1.0&quot;&quot; encoding=&quot;&quot;utf-8&quot;&quot;?&gt;
&lt;!DOCTYPE root [
&lt;!ELEMENT contentBase ANY&gt;
&lt;!ELEMENT home ANY&gt;
&lt;!ATTLIST home id ID #REQUIRED&gt;
&lt;!ELEMENT page ANY&gt;
&lt;!ATTLIST page id ID #REQUIRED&gt;
]&gt;
&lt;root id=&quot;&quot;-1=&quot;&quot;&quot;&quot;&gt;
&lt;home id=&quot;&quot;1103=&quot;&quot;&quot;&quot; key=&quot;&quot;156f1933-e327-4dce-b665-110d62720d03=&quot;&quot;&quot;&quot; parentID=&quot;&quot;-1=&quot;&quot;&quot;&quot; level=&quot;&quot;1=&quot;&quot;&quot;&quot; creatorID=&quot;&quot;0=&quot;&quot;&quot;&quot; sortOrder=&quot;&quot;0=&quot;&quot;&quot;&quot; createDate=&quot;&quot;2017-10-17T20:25:12=&quot;&quot;&quot;&quot; updateDate=&quot;&quot;2017-10-17T20:25:17=&quot;&quot;&quot;&quot; nodeName=&quot;&quot;Home=&quot;&quot;&quot;&quot; urlName=&quot;&quot;home=&quot;&quot;&quot;&quot; path=&quot;&quot;-1,1103=&quot;&quot;&quot;&quot; isDoc=&quot;&quot;&quot;&quot; nodeType=&quot;&quot;1093=&quot;&quot;&quot;&quot; creatorName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerID=&quot;&quot;0=&quot;&quot;&quot;&quot; template=&quot;&quot;1064=&quot;&quot;&quot;&quot; nodeTypeAlias=&quot;&quot;home=&quot;&quot;&quot;&quot;&gt;
&lt;title&gt;Welcome!&lt;/title&gt;
&lt;excerptCount&gt;4&lt;/excerptCount&gt;
&lt;page id=&quot;&quot;1122=&quot;&quot;&quot;&quot; key=&quot;&quot;1cb33e0a-400a-4938-9547-b05a35739b8b=&quot;&quot;&quot;&quot; parentID=&quot;&quot;1103=&quot;&quot;&quot;&quot; level=&quot;&quot;2=&quot;&quot;&quot;&quot; creatorID=&quot;&quot;0=&quot;&quot;&quot;&quot; sortOrder=&quot;&quot;0=&quot;&quot;&quot;&quot; createDate=&quot;&quot;2017-10-17T20:25:12=&quot;&quot;&quot;&quot; updateDate=&quot;&quot;2017-10-17T20:25:17=&quot;&quot;&quot;&quot; nodeName=&quot;&quot;Page=&quot;&quot; 1=&quot;&quot;&quot;&quot; urlName=&quot;&quot;page1=&quot;&quot;&quot;&quot; path=&quot;&quot;-1,1103,1122=&quot;&quot;&quot;&quot; isDoc=&quot;&quot;&quot;&quot; nodeType=&quot;&quot;1095=&quot;&quot;&quot;&quot; creatorName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerID=&quot;&quot;0=&quot;&quot;&quot;&quot; template=&quot;&quot;1060=&quot;&quot;&quot;&quot; nodeTypeAlias=&quot;&quot;page=&quot;&quot;&quot;&quot;&gt;
&lt;title&gt;Welcome!&lt;/title&gt;
&lt;excerpt&gt;[CDATA[Lorem ipsum dolor...]]&lt;/excerpt&gt;
&lt;body&gt;
[CDATA[&lt;p&gt;Lorem ipsum dolor...&lt;/p&gt;]]
&lt;/body&gt;
&lt;image&gt;123&lt;/image&gt;
&lt;/page&gt;
&lt;page id=&quot;&quot;1123=&quot;&quot;&quot;&quot; key=&quot;&quot;242928f6-a1cf-4cd3-ac34-f3ddf3526b2e=&quot;&quot;&quot;&quot; parentID=&quot;&quot;1103=&quot;&quot;&quot;&quot; level=&quot;&quot;2=&quot;&quot;&quot;&quot; creatorID=&quot;&quot;0=&quot;&quot;&quot;&quot; sortOrder=&quot;&quot;1=&quot;&quot;&quot;&quot; createDate=&quot;&quot;2017-10-17T20:25:12=&quot;&quot;&quot;&quot; updateDate=&quot;&quot;2017-10-17T20:25:17=&quot;&quot;&quot;&quot; nodeName=&quot;&quot;Page=&quot;&quot; 2=&quot;&quot;&quot;&quot; urlName=&quot;&quot;page2=&quot;&quot;&quot;&quot; path=&quot;&quot;-1,1103,1123=&quot;&quot;&quot;&quot; isDoc=&quot;&quot;&quot;&quot; nodeType=&quot;&quot;1095=&quot;&quot;&quot;&quot; creatorName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerName=&quot;&quot;Admin=&quot;&quot;&quot;&quot; writerID=&quot;&quot;0=&quot;&quot;&quot;&quot; template=&quot;&quot;1060=&quot;&quot;&quot;&quot; nodeTypeAlias=&quot;&quot;page=&quot;&quot;&quot;&quot;&gt;
&lt;title&gt;More welcome!&lt;/title&gt;
&lt;excerpt&gt;[CDATA[More lorem ipsum dolor...]]&lt;/excerpt&gt;
&lt;body&gt;[CDATA[Even more lorem ipsum dolor...]]&lt;/body&gt;
&lt;image&gt;234&lt;/image&gt;
&lt;/page&gt;
&lt;/home&gt;
&lt;/root&gt;
&quot;.Trim();
}
</code></pre>
<p>In our tests, we can now query by anything. The returned content has the hierarchy and everything, so we can traverse it with <code>Children()</code>, <code>Parent()</code> and whatnot.
The only missing piece is the properties. Here's a test showing that we have everything but the title property of Page 1:</p>
<pre><code>const int Page1Id = 1122;
[Test]
public void Returns_Empty_Documents()
{
var contentCache = umbracoSupport.UmbracoContext.ContentCache;
var page1 = contentCache.GetById(Page1Id);
Assert.That(page1, Is
.Not.Null
.And
.InstanceOf&lt;PublishedContentWithKeyBase&gt;()
.And
.Property(&quot;Name&quot;).EqualTo(&quot;Page 1&quot;)
.And
.Matches&lt;IPublishedContent&gt;(c =&gt; c[&quot;title&quot;] == null)
.And
.Property(&quot;Parent&quot;)
.Property(&quot;Children&quot;)
.With.Count.EqualTo(2)
);
}
</code></pre>
<p>Don't be discouraged though. This method is excellent for testing URL providers, ContentFinders, Menus, Sitemaps. You name it. I know I've written my fair share of hierarchy traversing code or fancy XPath queries. Unless of course, you need property values.</p>
<p>Instead of pulling your leg about it, here's how we fix that.</p>
<h2>Put some meat on the content</h2>
<p>The reason the properties are not there isn't because they weren't read. It's because the <code>XmlPublishedContent</code> that we get out ultimately relies on the <code>PublishedContentType</code> for it's respective document type. Luckily, all Umbraco's services are already stubbed up for us, so we can give it what it needs.</p>
<p>The gory guts of it is that it needs an <code>IContentType</code> from the <code>ContentTypeService</code>. We can easily stub one up with Moq: <code>var contentType = Mock.Of&lt;IContentType&gt;()</code>. Further, it uses the <code>IContentType.CompositionPropertyTypes</code> collection to iterate the properties. These <code>PropertyType</code> instances are actually completely dependency-less, so we can just create some:</p>
<pre><code>Mock.Get(contentType)
.Setup(t =&gt; t.CompositionPropertyTypes)
.Returns(new[] {
new PropertyType(&quot;Umbraco.TinyMCEv3&quot;, DataTypeDatabaseType.Nvarchar, &quot;body&quot;)
});
</code></pre>
<p>Finally, we set it up on the <code>ContentTypeService</code> stub:</p>
<pre><code>Mock.Get(umbracoSupport.ServiceContext.ContentTypeService)
.Setup(s =&gt; s.GetContentType(alias))
.Returns(contentType);
</code></pre>
<p>If only it were so easy. We depend on the <code>BaseWebTest</code> class from <code>Umbraco.Tests</code>. It sets up a content type factory that's being used somewhere in the hierarchy. It feeds <code>AutoPublishedContent</code> instances instead of what we've stubbed up. We need to turn that off. There's a trick here. <code>UmbracoSupport</code> should now live in an assembly called <code>Umbraco.UnitTests.Adapter</code>. If you're pre 7.6.4 you need to go with <code>Umbraco.VisualStudio</code>. This is because the factory we need to reset is internal to Umbraco. By having <code>UmbracoSupport</code> in an assembly with one of these two names, we're able to do it. (Otherwise, you use reflection.) <em>By no means do this with production code. Just... forget it!</em><br />
This paragraph should also get it's own blog post. :)</p>
<p>But I digress. Here's the line you need to have the content use the <code>ContentTypeService</code> to fetch its type:</p>
<pre><code>PublishedContentType.GetPublishedContentTypeCallback = null;
</code></pre>
<p>It's tempting to leave setup code like this lying around in all our <code>SetUp</code> methods or even in our &quot;Arrange&quot; sections. I've sinned too much, so those few lines are now part of <code>UmbracoSupport</code> and can be used to set up multiple types for your fixture or test.</p>
<p>Here's a test that fetches the same document as before, but can now read properties:</p>
<pre><code>[Test]
public void With_DocumentTypes_Setup_Returns_Full_Blown_Documents()
{
umbracoSupport.SetupContentType(&quot;page&quot;, new[]
{
new PropertyType(&quot;textstring&quot;, DataTypeDatabaseType.Nvarchar, &quot;title&quot;),
new PropertyType(&quot;textarea&quot;, DataTypeDatabaseType.Nvarchar, &quot;excerpt&quot;),
new PropertyType(&quot;Umbraco.TinyMCEv3&quot;, DataTypeDatabaseType.Nvarchar, &quot;body&quot;),
new PropertyType(&quot;media&quot;, DataTypeDatabaseType.Integer, &quot;image&quot;)
});
var page1 = contentCache.GetById(Page1Id);
Assert.Multiple(() =&gt;
{
Assert.That(page1[&quot;title&quot;], Is.EqualTo(&quot;Welcome!&quot;));
Assert.That(page1[&quot;excerpt&quot;], Is.EqualTo(&quot;Lorem ipsum dolor...&quot;));
Assert.That(page1[&quot;body&quot;].ToString(), Is.EqualTo(&quot;&lt;p&gt;Lorem ipsum dolor...&lt;/p&gt;&quot;));
Assert.That(page1[&quot;image&quot;], Is.EqualTo(123));
});
}
</code></pre>
<p>Notice the .ToString() on the body. It's actually not a string, but some weird dynamic Umbraco thingy. I never saw that type before, but I didn't pursue it in time for this post. I don't want anything to do with it though, so let's storm on to the grand finale.</p>
<h2>Let's make them strong already!</h2>
<p>We're finally there. The last piece of the puzzle. Strongly typed content!</p>
<p>It's managed by two resolvers: <code>PublishedContentModelFactoryResolver</code> and <code>PropertyValueConvertersResolver</code>. I won't go into details about those now, but suffice to say all resolvers have to be initialized before <code>BaseWebTest.Initialize</code> and its ancestors.
I've added an <code>InitializeResolvers</code> method to the <code>UmbracoSupport</code> class where these two are initialized. The <code>PublishedContentModelFactoryResolver</code> is set to a <code>FakeModelFactoryResolver</code> that lets you register constructors for document type aliases. <a href="https://github.com/lars-erik/umbraco-unit-testing-samples/blob/master/Umbraco.UnitTesting.Adapter/Support/FakeTypedModelFactory.cs">The code for this is available in my &quot;Umbraco unit testing samples&quot; repo on github</a>. </p>
<p>To set up property value converters, we also need to do that before registering the resolver. The resolver takes all the converters as constructor arguments. I've added a list of those types as a property on <code>UmbracoSupport</code>, so we can add <code>IPropertyValueConverter</code> implementing types before calling <code>UmbracoSupport.SetupUmbraco</code>:</p>
<pre><code>[SetUp]
public void Setup()
{
umbracoSupport = new UmbracoSupport();
// Converter types need to be added before setup
umbracoSupport.ConverterTypes.Add(typeof(TinyMceValueConverter));
umbracoSupport.SetupUmbraco();
//...
}
</code></pre>
<p>To register the typed model, there's just one line you can do in your setup, or even in your tests. Here I've refactored out the setup for the content type from earlier, and I register a model type for the document type alias:</p>
<pre><code>private void SetupContentType()
{
umbracoSupport.SetupContentType(&quot;page&quot;, new[]
{
new PropertyType(&quot;textstring&quot;, DataTypeDatabaseType.Nvarchar, &quot;title&quot;),
new PropertyType(&quot;textarea&quot;, DataTypeDatabaseType.Nvarchar, &quot;excerpt&quot;),
new PropertyType(&quot;Umbraco.TinyMCEv3&quot;, DataTypeDatabaseType.Nvarchar, &quot;body&quot;),
new PropertyType(&quot;media&quot;, DataTypeDatabaseType.Integer, &quot;image&quot;)
});
}
[Test]
public void With_DocumentTypes_And_Models_Setup_Returns_Fully_Functional_Typed_Content()
{
SetupContentType();
// Register strongly typed models with the ModelFactory
umbracoSupport.ModelFactory.Register(&quot;page&quot;, c =&gt; new Page(c));
var page1 = contentCache.GetById(Page1Id);
Assert.That(page1, Is
.InstanceOf&lt;Page&gt;()
.And.Property(&quot;Body&quot;)
.Matches&lt;IHtmlString&gt;(s =&gt;
s.ToString() == &quot;&lt;p&gt;Lorem ipsum dolor...&lt;/p&gt;&quot;
)
);
}
public class Page : PublishedContentModel
{
public Page(IPublishedContent content) : base((IPublishedContentWithKey)content)
{
}
public string Title =&gt; Content.GetPropertyValue&lt;string&gt;(&quot;title&quot;);
public string Excerpt =&gt; Content.GetPropertyValue&lt;string&gt;(&quot;excerpt&quot;);
public IHtmlString Body =&gt; Content.GetPropertyValue&lt;IHtmlString&gt;(&quot;body&quot;);
public int Image =&gt; Content.GetPropertyValue&lt;int&gt;(&quot;image&quot;);
}
</code></pre>
<p>There you go! There's nothing more to it. Well, there is...</p>
<p>The <code>Page</code> class here is bundled with the test. If we use a common interface both for our runtime model and our test model, we're safe. But we should really use the runtime models. This means you shouldn't use <em>runtime generated</em> models. <a href="https://github.com/zpqrtbnk/Zbu.ModelsBuilder/wiki/Install-And-Configure">Go through the instructions for ModelsBuilder</a> to have your models compiled and accessible from the tests.</p>
<h2>Conclusion</h2>
<p>And although the XML is pretty ugly, you can flush it out into files bundled with your tests. You can also exploit the umbraco.config file and just copy segments from there into your test source files. That way, you spend no time writing the stubs, and the content is cleanly separated from your tests.</p>
<p>That's <em>really</em> all there is to it! It is. Now go test a bit, or a byte, or a string, or even a <a href="/archive/testing-views-with-razorgenerator/">view</a>.</p>
<p><a href="https://github.com/lars-erik/umbraco-unit-testing-samples/tree/master/Umbraco.UnitTesting.Adapter/Support">The new version of UmbracoSupport including the fake model factory is available here.</a></p>
Tue, 17 Oct 2017 21:18:06 Z2017-10-17T21:18:06Z1126http://blog.aabech.no/archive/testing-views-with-razorgenerator/
unit testingTesting views with RazorGenerator<h2>Razor what?</h2>
<p>RazorGenerator is a hidden gem that lets you generate and pre-compile what would otherwise be generated and compiled at runtime. Your Razor. Not only does it give you a startup time boost, but it lets you unit test your views. The latter is the focus of this post.</p>
<p>We'll continue to build on the project I described in <a href="/archive/generating-documentation-from-nunit-tests/">my post on generating documentation with NUnit</a>. It's a simple use case where an imaginary CMS has a feature to display events from a third party site. Here are the tests I've got so far:</p>
<p><img src="http://blog.aabech.no/blog/media/1024/nunitoutput.png" alt="NUnit output" /></p>
<p>In my previous post, I left the rendering test inconclusive. I like keeping inconclusive tests around as reminders of stuff I've got left to do. Let's have a quick look at the passing code before we dive into the rendering bits.</p>
<h2>Basic HTTP integration and conversion tests</h2>
<p>The first large piece of what's in there for now is a way to remove the physical dependency on the third party site. I like to stub away IO as far out as I can so I can test as much of my code quickly, yet as integrated as possible. In other words, as many participating classes as possible. Since we're making an example here, I'm just using the <code>HttpClient</code> directly from the controller. The <code>HttpClient</code> is hard to mock or fake, but it has an inner dependency that we can pass as an argument to a constructor: <code>HttpMessageHandler</code>. It has the innermost function that the <code>HttpClient</code> uses for any operation. It also has the rare trait of being <code>protected virtual</code>, so we can stub it out. For this example, I'm just using a fake one that records requests and returns responses for registered URLs. Here it is:</p>
<pre><code>public class FakeMessageHandler : HttpMessageHandler
{
private readonly Dictionary&lt;string, string&gt; responses = new Dictionary&lt;string,string&gt;();
public List&lt;string&gt; Calls { get; } = new List&lt;string&gt;();
public void Register(string url, string response)
{
responses.Add(url, response);
}
protected override Task&lt;HttpResponseMessage&gt; SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var url = request.RequestUri.ToString();
Calls.Add(url);
if (!responses.ContainsKey(url))
{
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent(&quot;{}&quot;)
});
}
return Task.FromResult(new HttpResponseMessage
{
Content = new StringContent(responses[url])
});
}
}
</code></pre>
<p>We'll not dwell further on this one in this post, but you'll need it to run the following examples if you want to tag along.</p>
<p>The tests that verifies that we call the right URL on the third party site is pretty simple. It checks the Calls collection on the FakeMessageHandler. Here's the test and the setup code needed:</p>
<pre><code>[TestFixture]
public class When_Displaying_An_Event
{
[Test]
[DocumentationOrder(0)]
[Description(@&quot;
Events are provided at eventsite with a REST api at the URL:
https://eventsite/api/{id}
&quot;)]
public void It_Is_Fetched_By_Id_From_The_Event_Server()
{
eventController.Event(234).Wait();
Assert.AreEqual(
&quot;https://eventsite/api/234&quot;,
httpMessageHandler.Calls.Single()
);
}
// ... omitted other tests
[SetUp]
public void Setup()
{
httpMessageHandler = new FakeMessageHandler();
httpClient = new HttpClient(httpMessageHandler);
eventController = new EventController(httpClient);
}
private FakeMessageHandler httpMessageHandler;
private HttpClient httpClient;
private EventController eventController;
}
</code></pre>
<p>And the controller:</p>
<pre><code>public class EventController : Controller
{
private readonly HttpClient httpClient;
public EventController(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task&lt;PartialViewResult&gt; Event(int id)
{
var requestUri = &quot;https://eventsite/api/&quot; + id;
var result = await httpClient.GetAsync(requestUri);
var response = await result.Content.ReadAsStringAsync();
// ... omitted further processing for now
}
}
</code></pre>
<p>We pass the <code>FakeMessageHandler</code> instance to a new <code>HttpClient</code> effectively making it a stub. We can control the response for individual URLs, and verify that the right ones were called.</p>
<p>Next step is to convert it to a valid model we can use in our view. We've got a second test asserting that:</p>
<pre><code>[Test]
[DocumentationOrder(1)]
[Description(&quot;The JSON response from the event server is deserialized as the Event type.&quot;)]
public void It_Is_Converted_To_An_Event_Model()
{
var result = GetConcertEvent();
var model = (Event)result.Model;
Assert.AreEqual(&quot;Concert of your life&quot;, model.Name);
Assert.AreEqual(new DateTime(2049,12,31,23,59,59), model.Time);
Assert.AreEqual(&quot;https://eventsite/123&quot;, model.Url);
}
private PartialViewResult GetConcertEvent()
{
httpMessageHandler.Register(
&quot;https://eventsite/api/123&quot;,
&quot;{\&quot;name\&quot;:\&quot;Concert of your life\&quot;, \&quot;time\&quot;:2524607999}&quot;
);
var result = eventController.Event(123).Result;
return result;
}
</code></pre>
<p>And here's the rest of the controller code creating that model:</p>
<pre><code>public async Task&lt;PartialViewResult&gt; Event(int id)
{
var requestUri = &quot;https://eventsite/api/&quot; + id;
var result = await httpClient.GetAsync(requestUri);
var response = await result.Content.ReadAsStringAsync();
var eventJObj = JsonConvert.DeserializeObject&lt;JObject&gt;(response);
var evt = new Event
{
Name = eventJObj.Value&lt;string&gt;(&quot;name&quot;),
Time = DateTimeOffset.FromUnixTimeSeconds(eventJObj.Value&lt;long&gt;(&quot;time&quot;)).DateTime,
Url = &quot;https://eventsite/&quot; + id
};
return PartialView(&quot;Event&quot;, evt);
}
</code></pre>
<p>The test is a tedious verification of every property on the event object. There are several ways to get around that. Amongst others, equality members. I've got a way better trick, but that's for an upcoming post.</p>
<p>Now that we're through those tests, let's dive into how we can test the actual HTML output of this whole shenanigan.</p>
<h2>Generating some Razor</h2>
<p>As mentioned initially, RazorGenerator is a hidden gem in the ASP.NET OSS wilderness. There's a Visual Studio plugin that you need to exploit it fully. It's aptly called RazorGenerator and <a href="https://marketplace.visualstudio.com/items?itemName=DavidEbbo.RazorGenerator">can be installed from here</a>. There's also a couple of Nuget packages that we want:</p>
<ul>
<li><a href="https://www.nuget.org/packages/RazorGenerator.Mvc/">RazorGenerator.Mvc</a><br />
Referenced in your website to make use of a special <code>ViewEngine</code> for pre-compiled views.</li>
<li><a href="https://www.nuget.org/packages/RazorGenerator.Testing/">RazorGenerator.Testing</a><br />
Referenced in our test projects to be able to automagically render views in tests.</li>
</ul>
<p>Armed with these tools, there's nothing left to do but add a custom tool to our view:</p>
<p><img src="http://blog.aabech.no/blog/media/1029/razorgenerator_tool.png" alt="RazorGenerator as Custom Tool for a view" /></p>
<p>As soon as you've blured the &quot;Custom Tool&quot; field, a .cs file will be generated beneath the view. Upon inspection it yields a class in the ASP namespace making a whole lot of Write* statements. This is what clutters up your ASP.NET Temporary files all the time. And the nice part: it can be instantiated. </p>
<p>Here's the initial markup from Event.cshtml:</p>
<pre><code>@model Example.WebSite.Models.Event
&lt;div&gt;
&lt;a href=&quot;@Model.Url&quot;&gt;
&lt;label&gt;
@Model.Name
&lt;/label&gt;
&lt;span&gt;
@Model.Time.ToString(&quot;f&quot;)
&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>Over in our unit test, we can now start adding some actual rendering of HTML:</p>
<pre><code>[Test]
[DocumentationOrder(2)]
public void It_Is_Rendered_With_A_Name_Date_And_A_Link()
{
var view = new _Views_Partials_Event_cshtml();
var result = GetConcertEvent();
Assert.AreEqual(&quot;Event&quot;, result.ViewName);
var renderedResult = view.Render((Event) result.Model);
Console.WriteLine(renderedResult);
Assert.Inconclusive(&quot;Rendering has not been implemented yet.&quot;);
}
private PartialViewResult GetConcertEvent()
{
httpMessageHandler.Register(
&quot;https://eventsite/api/123&quot;,
&quot;{\&quot;name\&quot;:\&quot;Concert of your life\&quot;, \&quot;time\&quot;:2524607999}&quot;
);
var result = eventController.Event(123).Result;
return result;
}
</code></pre>
<p>I know, the view type name ain't too pretty. But it's necessary for the view engine to find the right one based on the path given. The cool part is the <code>Render</code> statement. It's an extension method from the RazorGenerator.Tests package. It returns the final HTML as a string.</p>
<p>The <code>Console.WriteLine</code> statement yields the following in our test output now:</p>
<pre><code>&lt;div&gt;
&lt;a href=&quot;https://eventsite/123&quot;&gt;
&lt;label&gt;
Concert of your life
&lt;/label&gt;
&lt;span&gt;
fredag 31. desember 2049 23.59
&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p><em>(Yes, I force my locale upon thee!)</em></p>
<p>What we've just done is to test our system almost end to end, cut off at the uttermost borders to external IO. Specifically, the third party site and the end-user's browser. </p>
<p>Granted, we could do Selenium tests here to really test it via the browser, but my rule of thumb is that RazorGenerator is the best choice unless you've got a lot of JavaScript interactivity you need to test in integration. These are subjects for another post.</p>
<p>There is a remaining issue though. We should assert the HTML we got rendered. We could store that HTML we got in a <code>const string expected</code> in our test, but it's gonna foul up the code quite a bit. We could go with so-called &quot;gold files&quot; and implement something to compare a file to the actual output. There's a magical tool for that called ApprovalTests, which I'll cover in my next post. </p>
<p>There's also the option of using HtmlAgilityPack to query the rendered view. The RazorGenerator.Tests package have built-in alternative for <code>Render</code> called <code>RenderAsHtml</code> that returns <code>HtmlDocument</code> instances for you to query. It's quite useful when your only <code>Assert.That</code> is for some specific element in a big page.</p>
<h2>Resources and a small limitation</h2>
<p>You've seen how you can use RazorGenerator to test your views. <a href="http://blog.davidebbo.com/tag/#RazorGenerator">There are several posts by David Ebbo (one of the authors of RazorGenerator) on how to use RazorGenerator. Please check them out for more details than I was able to give here.</a></p>
<p>For now it doesn't do nested <code>Html.Action</code> or <code>Html.Partial</code> calls. I've got a PR in the works, but I need to polish it for it to get in there. Some day soon. ;) If you really want to, you'll find my fork and build your own, but you'll be on your own.</p>
<p>There's also a tiny performance hit. You'll have to wait a second longer for your tests to execute, since a lot of the ASP.NET MVC framework is spun up to render views. It's still less than the magical 2 second focus cap though, so you should be able to work effectively with it.</p>
<p>I hope this piqued your interest in writing broader tests even up to the UI layers. There's even cooler tricks in store for you if you're already on .net Core, but the rest of us will have to make due until then.</p>
Mon, 09 Oct 2017 21:08:20 Z2017-10-09T21:08:20Z1118http://blog.aabech.no/archive/generating-documentation-from-nunit-tests/
unit testingGenerating documentation from NUnit tests<h2>What's the catch?</h2>
<p>There's a lot of benefits with unit testing. One of the less exploited ones would be documentation.
I've heard it said, and I've said it myself countless times: Unit Tests can be used as documentation.
They are inherently documentation by being a good place to look for sample code.
But what if they could also serve as actual go-to documentation, a backlog, a technical specification and all those things that otherwise go rot in a Word document on some server?</p>
<p>Lately, I've been experimenting with generating good documentation from NUnit tests for our ASP.NET MVC sites. (Based on Umbraco CMS, but that's not important to this article.)<br />
I've been baffled by the lack of good information when I've googled for NUnit result transformations. So I gave it a go, and I'll share some techniques with you in this post.</p>
<p>Many unit tests won't do for documentation though. I believe that testing should be done as integrated as possible, stopping just before they become slow. This allows for more user-centric, or holistic descriptions of the system. I'm doing my best to get better at naming them. They should read like stories describing the system. That way it'll be easy for other devs to find the right code to extend or debug. Namespacing and organization play a strong role, as well as good, descriptive names. It's hard, but I believe striving for this might actually also aid in the ability to use them as documentation. </p>
<h2>An example</h2>
<p>So for this example, let's have a look at a pretty simple case. We're integrating with a third-party event site to display relevant events on pages in our site. We have some kind of module that lets editors pick events and place them in pages. The part I'll use in this post is the part about fetching and displaying it.
</p>
<p>The tests I'm writing are feature specific, but technical enough in nature to belong in the realm of technical documentation. For business targeted documentation, you're better off looking into <a href="https://cucumber.io/">BDD and all the practices that follows</a>. If you don't have the team though, you might be able to get something working for you with the following techniques.</p>
<p>Here's the test output I've got so far:</p>
<p><img src="http://blog.aabech.no/blog/media/1024/nunitoutput.png" alt="NUnit output" /></p>
<p>I think it reads fairly well. There shouldn't be too much doubt what this feature does. Granted, it's a really simple one, and complex systems will have thousands. Even more reason to group by feature, and add areas and other useful grouping levels as namespaces.</p>
<p><em>You'll also notice I've got a test that is inconclusive saying not implemented. I've started doing this to keep a kind of backlog of things to do. It's easy to forget about technical debt or rarer cases we've ignored. Having inconclusive tests glaring at you makes it easy to remember. It may also serve as a discussion point to bring up with the customer. &quot;Should we bother to handle this weird case? It'll increase the budget with $...&quot;.</em></p>
<p>There are still a couple of readability issues in the tests. When we add more features, it'd be nice to find all event-related tests in their own group. That's a quick fix. We'll add a namespace. (Put common tests in the same folder)</p>
<p><img src="http://blog.aabech.no/blog/media/1025/nunitoutput_with_namespace.png" alt="NUnit output with namespace" /></p>
<p>There's also a matter of order. Obviously fetching from server happens before converting to a model and then rendering it. There are concepts like ordered tests, but I believe atomic tests are faster and more flexible. I've got a solution for that, but let's live with it for now.</p>
<p>Having this output from the testrunner or on the build server, even in your inbox when someone commits, is useful. We have a broad overview of what the system does. The tests names are decoupled from any implementation details so we're free to change the architecture while keeping our descriptive tests.</p>
<p>But we could do more.</p>
<h2>NUnit console runner output</h2>
<p>When working on code, we use built-in test runners in our IDEs. We're able to run one or a few test in isolation and focus on just the right things. Build servers use command line runners and ususally produce some form of textual or HTML output. We can exploit the CLI tools as well. The NUnit console runner outputs an XML format by default. You can even do it from the package manager console in Visual Studio like so:</p>
<pre><code>PM&gt; .\packages\NUnit.ConsoleRunner.3.7.0\tools\nunit3-console.exe .\NUnitDocs.Tests\bin\debug\NUnitDocs.Tests.dll
</code></pre>
<p><em>If you're using Nuget to organize your dependencies, you can install the NUnit.ConsoleRunner package to get the executable in the packages folder. Otherwise, you can download it from the <a href="http://nunit.org/download/">NUnit downloads page</a>.</em></p>
<p>In addition to logging the results to the output, NUnit will write some metadata, and the following (more interesting) to TestOutput.xml:</p>
<pre><code>&lt;test-run id=&quot;2&quot; testcasecount=&quot;3&quot; result=&quot;Passed&quot; total=&quot;3&quot; passed=&quot;2&quot; failed=&quot;0&quot; inconclusive=&quot;1&quot; &gt;
&lt;!-- Attributes and metadata elements stripped for clarity --&gt;
&lt;test-suite type=&quot;TestSuite&quot; id=&quot;0-1005&quot; name=&quot;NUnitDocs&quot; fullname=&quot;NUnitDocs&quot; runstate=&quot;Runnable&quot; testcasecount=&quot;3&quot; result=&quot;Passed&quot; total=&quot;3&quot; passed=&quot;2&quot; failed=&quot;0&quot; warnings=&quot;0&quot; inconclusive=&quot;1&quot;&gt;
&lt;test-suite type=&quot;TestSuite&quot; id=&quot;0-1006&quot; name=&quot;Tests&quot; fullname=&quot;NUnitDocs.Tests&quot;&gt;
&lt;test-suite type=&quot;TestSuite&quot; id=&quot;0-1007&quot; name=&quot;Events&quot; fullname=&quot;NUnitDocs.Tests.Events&quot;&gt;
&lt;test-suite type=&quot;TestFixture&quot; id=&quot;0-1000&quot; name=&quot;When_Displaying_An_Event&quot; fullname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event&quot;&gt;
&lt;test-case id=&quot;0-1002&quot; name=&quot;It_Is_Converted_To_An_Event_Model&quot; fullname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event.It_Is_Converted_To_An_Event_Model&quot; methodname=&quot;It_Is_Converted_To_An_Event_Model&quot; classname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event&quot; runstate=&quot;Runnable&quot; seed=&quot;1189788506&quot; result=&quot;Passed&quot; start-time=&quot;2017-10-08 19:44:11Z&quot; end-time=&quot;2017-10-08 19:44:12Z&quot; duration=&quot;0.413085&quot; asserts=&quot;3&quot; /&gt;
&lt;test-case id=&quot;0-1001&quot; name=&quot;It_Is_Fetched_By_Id_From_The_Event_Server&quot; fullname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event.It_Is_Fetched_By_Id_From_The_Event_Server&quot; methodname=&quot;It_Is_Fetched_By_Id_From_The_Event_Server&quot; classname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event&quot; runstate=&quot;Runnable&quot; seed=&quot;486335788&quot; result=&quot;Passed&quot; start-time=&quot;2017-10-08 19:44:12Z&quot; end-time=&quot;2017-10-08 19:44:12Z&quot; duration=&quot;0.001595&quot; asserts=&quot;1&quot; /&gt;
&lt;test-case id=&quot;0-1003&quot; name=&quot;It_Is_Rendered_With_A_Name_Date_And_A_Link&quot; fullname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event.It_Is_Rendered_With_A_Name_Date_And_A_Link&quot; methodname=&quot;It_Is_Rendered_With_A_Name_Date_And_A_Link&quot; classname=&quot;NUnitDocs.Tests.Events.When_Displaying_An_Event&quot; runstate=&quot;Runnable&quot; seed=&quot;804806648&quot; result=&quot;Inconclusive&quot; start-time=&quot;2017-10-08 19:44:12Z&quot; end-time=&quot;2017-10-08 19:44:12Z&quot; duration=&quot;0.015905&quot; asserts=&quot;0&quot;&gt;
&lt;reason&gt;
&lt;message&gt;
&lt;![CDATA[Rendering has not been implemented yet.]]/&gt;
&lt;/message&gt;
&lt;/reason&gt;
&lt;/test-case&gt;
&lt;/test-suite&gt;
&lt;/test-suite&gt;
&lt;/test-suite&gt;
&lt;/test-suite&gt;
&lt;/test-run&gt;
</code></pre>
<p>I know, I know. It's not cool anymore. I don't like it any more than you, but there is one durable ancient technology we could use to do something with this output. Did you guess it yet?</p>
<h2>XSLT!</h2>
<p>There. I said it.</p>
<p>Now let's look at a simple output before we dive into the more beefy things:</p>
<p><img src="http://blog.aabech.no/blog/media/1026/transformed_output.png" alt="Transformed output" /></p>
<p>Now ain't that starting to look readable? There's not that much to it. The NUnit console runner has a built-in parameter with the syntax --result=SPEC, where SPEC can point to an XSLT sheet. Here's the command to generate the former:</p>
<pre><code>.\packages\NUnit.ConsoleRunner.3.7.0\tools\nunit3-console.exe .\NUnitDocs.Tests\bin\debug\NUnitDocs.Tests.dll --result:&quot;TestSummary.htm;transform=TestSummary.xslt&quot;
</code></pre>
<p>The XSLT isn't much either. There's Bootstrap and (our trusty) JQuery from CDN. Some simple HTML formatting. And I wrote a one-liner JavaScript function to strip out all the underscores as well. Here's the first pass:</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;iso-8859-1&quot;?&gt;
&lt;xsl:stylesheet
version=&quot;1.0&quot;
exclude-result-prefixes=&quot;msxsl&quot;
xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot;
xmlns:msxsl=&quot;urn:schemas-microsoft-com:xslt&quot;
&gt;
&lt;xsl:output method=&quot;html&quot; indent=&quot;yes&quot;/&gt;
&lt;xsl:template match=&quot;@* | node()&quot;&gt;
&lt;html&gt;
&lt;head&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css&quot; integrity=&quot;sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u&quot; crossorigin=&quot;anonymous&quot;/&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css&quot; integrity=&quot;sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp&quot; crossorigin=&quot;anonymous&quot;/&gt;
&lt;style type=&quot;text/css&quot;&gt;
.Passed { color: green; }
.Inconclusive { color: #BBAA00; }
.Failed { color: red; }
ul {
margin-left: 0px;
list-style-type: none;
padding-left: 0;
}
ul ul {
margin-left: 15px;
}
.counts {
font-size: .7em;
color: gray;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;xsl:apply-templates select=&quot;/test-run/test-suite&quot;/&gt;
&lt;/div&gt;
&lt;script src=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js&quot; integrity=&quot;sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa&quot; crossorigin=&quot;anonymous&quot;&gt;// Force closing tag&lt;/script&gt;
&lt;script src=&quot;https://code.jquery.com/jquery-3.2.1.slim.min.js&quot;&gt;// Force closing tag&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
$(&quot;td&quot;).each(function(i, e) {
$(e).text($(e).text().replace(/_/g, &quot; &quot;));
});
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;/test-run/test-suite&quot;&gt;
&lt;h1&gt;
&lt;xsl:value-of select=&quot;./test-run/@name&quot;/&gt;
&lt;/h1&gt;
&lt;table class=&quot;table table-striped&quot;&gt;
&lt;xsl:apply-templates select=&quot;./test-suite&quot;/&gt;
&lt;/table&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;test-suite&quot;&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;xsl:attribute name=&quot;class&quot;&gt;
&lt;xsl:choose&gt;
&lt;xsl:when test=&quot;./@failed &gt; 0&quot;&gt;Failed&lt;/xsl:when&gt;
&lt;xsl:when test=&quot;./@inconclusive &gt; 0&quot;&gt;Inconclusive&lt;/xsl:when&gt;
&lt;xsl:otherwise&gt;Passed&lt;/xsl:otherwise&gt;
&lt;/xsl:choose&gt;
&lt;/xsl:attribute&gt;
&lt;xsl:attribute name=&quot;style&quot;&gt;
padding-left: &lt;xsl:value-of select=&quot;count(ancestor::test-suite)*15&quot;/&gt;px;
&lt;/xsl:attribute&gt;
&lt;xsl:value-of select=&quot;./@name&quot;/&gt;
&lt;/td&gt;
&lt;td class=&quot;counts&quot;&gt;
&lt;xsl:value-of select=&quot;./@passed&quot;/&gt; passed,
&lt;xsl:value-of select=&quot;./@inconclusive&quot;/&gt; inconclusive,
&lt;xsl:value-of select=&quot;./@failed&quot;/&gt; failed
&lt;/td&gt;
&lt;/tr&gt;
&lt;xsl:for-each select=&quot;./test-suite&quot;&gt;
&lt;xsl:apply-templates select=&quot;.&quot;/&gt;
&lt;/xsl:for-each&gt;
&lt;xsl:for-each select=&quot;./test-case&quot;&gt;
&lt;xsl:apply-templates select=&quot;.&quot;/&gt;
&lt;/xsl:for-each&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;test-case&quot;&gt;
&lt;tr&gt;
&lt;td colspan=&quot;2&quot;&gt;
&lt;xsl:attribute name=&quot;style&quot;&gt;
padding-left: &lt;xsl:value-of select=&quot;count(ancestor::test-suite)*15&quot;/&gt;px;
&lt;/xsl:attribute&gt;
&lt;xsl:attribute name=&quot;class&quot;&gt;
&lt;xsl:value-of select=&quot;./@result&quot;/&gt;
&lt;/xsl:attribute&gt;
&lt;xsl:value-of select=&quot;./@name&quot;/&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/xsl:template&gt;
&lt;/xsl:stylesheet&gt;
</code></pre>
<p>This is fairly good as a starting point, but we're stuck with the brief test names. Having some free text, at least at feature level or use-case level would be nice. And we still have this ordering problem to deal with. NUnit has the perfect tool for this: </p>
<h2>NUnit PropertyAttribute</h2>
<p>NUnit comes with a <code>PropertyAttribute</code> that we can use to decorate our tests with metadata. It's main purpose is to add properties to fixtures and tests in the test output. It's an abstract class we can use to create our own metadata, and there's a few built-in like <code>DescriptionAttribute</code>. There are uses of properties at runtime too, but that's way too advanced for this post.</p>
<p>The <code>DescriptionAttribute</code> is just what we want for the free text part of our documentation. It's added to the method just like the <code>Test</code> attribute:</p>
<pre><code>[Test]
[Description(@&quot;
Events are provided at eventsite with a REST api at the URL:
https://eventsite/api/{id}
&quot;)]
public void It_Is_Fetched_By_Id_From_The_Event_Server()
{
// ...
}
[Test]
[Description(&quot;The JSON response from the event server is deserialized as the Event type.&quot;)]
public void It_Is_Converted_To_An_Event_Model()
{
// ...
}
</code></pre>
<p>The <code>Description</code> is written to the XML output like so:</p>
<pre><code>&lt;test-case id=&quot;0-1002&quot; name=&quot;It_Is_Converted_To_An_Event_Model&quot; ...&gt;
&lt;properties&gt;
&lt;property name=&quot;Description&quot; value=&quot;The JSON response from the event server is deserialized as the Event type.&quot; /&gt;
&lt;/properties&gt;
&lt;/test-case&gt;
</code></pre>
<p>By adding a few more voodoo lines to the XSLT and another one-liner JavaScript, we get the following output:<br />
<em>(Don't worry, complete XSLT is available at the bottom fo the post.)</em></p>
<p><img src="http://blog.aabech.no/blog/media/1028/transformed_output_with_description.png" alt="Transformed HTML output with descriptions" /></p>
<p>Now we're really getting somewhere. We can provide that little extra that tells the reader what's really happening and stuff that might be obscured by too many, or hopefully just the right amount of abstractions.</p>
<p>Let's tackle the ordering problem while we're at it.</p>
<h2>Implementing our own properties</h2>
<p>The PropertyAttribute isn't much more than a metadata container. An ordering property is as simple as such:</p>
<pre><code>public class DocumentationOrderAttribute : PropertyAttribute
{
public DocumentationOrderAttribute(int order)
: base(&quot;DocumentationOrder&quot;, order)
{
}
}
</code></pre>
<p>It's applied to the tests just like the descriptions:</p>
<pre><code>[Test]
[DocumentationOrder(0)]
[Description(@&quot;
Events are provided at eventsite with a REST api at the URL:
https://eventsite/api/{id}
&quot;)]
public void It_Is_Fetched_By_Id_From_The_Event_Server()
{
// ...
}
</code></pre>
<p>And it's output just like descriptions:</p>
<pre><code>&lt;test-case id=&quot;0-1002&quot; name=&quot;It_Is_Converted_To_An_Event_Model&quot; ...&gt;
&lt;properties&gt;
&lt;property name=&quot;DocumentationOrder&quot; value=&quot;1&quot; /&gt;
&lt;property name=&quot;Description&quot; value=&quot;The JSON response from the event server is deserialized as the Event type.&quot; /&gt;
&lt;/properties&gt;
&lt;/test-case&gt;
</code></pre>
<p>Now we can tackle the ordering problem with a simple XSLT sort element:</p>
<pre><code>&lt;xsl:for-each select=&quot;./test-case&quot;&gt;
&lt;xsl:sort select=&quot;./properties/property[@name='DocumentationOrder']/@value&quot;/&gt;
&lt;xsl:apply-templates select=&quot;.&quot;/&gt;
&lt;/xsl:for-each&gt;
</code></pre>
<p>And we get a nice ordered output:</p>
<p><img src="http://blog.aabech.no/blog/media/1027/transformed_output_ordered.png" alt="Transformed output ordered by attribute" /></p>
<h2>Summary</h2>
<p>By structuring and naming our tests by feature we get nice headings for potential documentation. We also untie them completely from implementation so we're free to change our code. Applying a bit of metadata to our tests adds that little extra to make quite meaningful documentation.</p>
<p>The fact that the results can be tranformed means we can create rich documentation UIs with search, tags, navigation structure and a bit less than just enough prose.</p>
<h2>Next steps</h2>
<p>We've still got that rendering test to pass. There's also a few too many asserts in one of the tests. In an upcoming post I'll share a couple of other tools with you and how to extend those to enrich the technical documentation even more.</p>
<h2>Complete XSLT</h2>
<p>Here's the complete XSLT for the examples above:</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;iso-8859-1&quot;?&gt;
&lt;xsl:stylesheet
version=&quot;1.0&quot;
exclude-result-prefixes=&quot;msxsl&quot;
xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot;
xmlns:msxsl=&quot;urn:schemas-microsoft-com:xslt&quot;
&gt;
&lt;xsl:output method=&quot;html&quot; indent=&quot;yes&quot;/&gt;
&lt;xsl:template match=&quot;@* | node()&quot;&gt;
&lt;html&gt;
&lt;head&gt;
&lt;!-- Latest compiled and minified CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css&quot; integrity=&quot;sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u&quot; crossorigin=&quot;anonymous&quot;/&gt;
&lt;!-- Optional theme --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css&quot; integrity=&quot;sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp&quot; crossorigin=&quot;anonymous&quot;/&gt;
&lt;style type=&quot;text/css&quot;&gt;
.Passed { color: green; }
.Inconclusive { color: #BBAA00; }
.Failed { color: red; }
ul {
margin-left: 0px;
list-style-type: none;
padding-left: 0;
}
ul ul {
margin-left: 15px;
}
label {
font-weight: normal;
}
.counts {
font-size: .7em;
color: gray;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;xsl:apply-templates select=&quot;/test-run/test-suite&quot;/&gt;
&lt;/div&gt;
&lt;!-- Latest compiled and minified JavaScript --&gt;
&lt;script src=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js&quot; integrity=&quot;sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa&quot; crossorigin=&quot;anonymous&quot;&gt;// Force closing tag&lt;/script&gt;
&lt;script src=&quot;https://code.jquery.com/jquery-3.2.1.slim.min.js&quot;&gt;// Force closing tag&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
$(&quot;td label&quot;).each(function(i, e) {
$(e).text($(e).text().replace(/_/g, &quot; &quot;));
});
$(&quot;.description&quot;).each(function(i, e) {
$(e).html($(e).html().trim().replace(/\n/g, '&lt;br/&gt;'));
});
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;/test-run/test-suite&quot;&gt;
&lt;h1&gt;
&lt;xsl:value-of select=&quot;./test-run/@name&quot;/&gt;
&lt;/h1&gt;
&lt;table class=&quot;table table-striped&quot;&gt;
&lt;xsl:apply-templates select=&quot;./test-suite&quot;/&gt;
&lt;/table&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;test-suite&quot;&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;xsl:attribute name=&quot;class&quot;&gt;
&lt;xsl:choose&gt;
&lt;xsl:when test=&quot;./@failed &gt; 0&quot;&gt;Failed&lt;/xsl:when&gt;
&lt;xsl:when test=&quot;./@inconclusive &gt; 0&quot;&gt;Inconclusive&lt;/xsl:when&gt;
&lt;xsl:otherwise&gt;Passed&lt;/xsl:otherwise&gt;
&lt;/xsl:choose&gt;
&lt;/xsl:attribute&gt;
&lt;xsl:attribute name=&quot;style&quot;&gt;
padding-left: &lt;xsl:value-of select=&quot;count(ancestor::test-suite)*15&quot;/&gt;px;
&lt;/xsl:attribute&gt;
&lt;xsl:value-of select=&quot;./@name&quot;/&gt;
&lt;/td&gt;
&lt;td class=&quot;counts&quot;&gt;
&lt;xsl:value-of select=&quot;./@passed&quot;/&gt; passed,
&lt;xsl:value-of select=&quot;./@inconclusive&quot;/&gt; inconclusive,
&lt;xsl:value-of select=&quot;./@failed&quot;/&gt; failed
&lt;/td&gt;
&lt;/tr&gt;
&lt;xsl:for-each select=&quot;./test-suite&quot;&gt;
&lt;xsl:apply-templates select=&quot;.&quot;/&gt;
&lt;/xsl:for-each&gt;
&lt;xsl:for-each select=&quot;./test-case&quot;&gt;
&lt;xsl:sort select=&quot;./properties/property[@name='DocumentationOrder']/@value&quot;/&gt;
&lt;xsl:apply-templates select=&quot;.&quot;/&gt;
&lt;/xsl:for-each&gt;
&lt;/xsl:template&gt;
&lt;xsl:template match=&quot;test-case&quot;&gt;
&lt;tr&gt;
&lt;td colspan=&quot;2&quot;&gt;
&lt;xsl:attribute name=&quot;style&quot;&gt;
padding-left: &lt;xsl:value-of select=&quot;count(ancestor::test-suite)*15&quot;/&gt;px;
&lt;/xsl:attribute&gt;
&lt;label&gt;
&lt;xsl:attribute name=&quot;class&quot;&gt;
&lt;xsl:value-of select=&quot;./@result&quot;/&gt;
&lt;/xsl:attribute&gt;
&lt;xsl:value-of select=&quot;./@name&quot;/&gt;
&lt;/label&gt;
&lt;xsl:if test=&quot;./properties/property[@name='Description']&quot;&gt;
&lt;div class=&quot;description&quot;&gt;
&lt;xsl:value-of select=&quot;./properties/property[@name='Description']/@value&quot;/&gt;
&lt;xsl:text&gt; &lt;/xsl:text&gt;
&lt;/div&gt;
&lt;/xsl:if&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/xsl:template&gt;
&lt;/xsl:stylesheet&gt;
</code></pre>
Sun, 08 Oct 2017 21:06:26 Z2017-10-08T21:06:26Z