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 Umbraco1127http://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:06Z