How to UnitTest your WebControls rendered HTML
Testing ASP.NET code is always hard. How do you test that a control renders the HTML that you require? I will show you how. Let's use the ULRadioButtonList that I have written about earlier and test that it outputs the expected markup.
The schema that validates the markup
The downside of using a XSD schema to validate HTML, is that HTML 5 is not XML as XHTML was. There's not much we can do about that except writing our controls markup as if it where strict XHTML.
<!-- Root element --> <xs:element name="ul"> <xs:complexType>
<!-- Unbounded number of elements -->
<xs:sequence maxOccurs="unbounded">
<xs:element ref="li" />
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- The LI element--> <xs:element id="li" name="li"> <xs:complexType> <xs:sequence> <xs:element ref="input" /> <xs:element ref="label" /> </xs:sequence> </xs:complexType> </xs:element>
<!-- input element has only attributes --> <xs:element id="input" name="input"> <xs:complexType> <xs:attribute name="id" type="xs:string"></xs:attribute> <xs:attribute name="name" type="xs:string"></xs:attribute> <xs:attribute name="type" type="xs:string" fixed="radio"></xs:attribute> <xs:attribute name="value" type="xs:string"></xs:attribute> </xs:complexType> </xs:element>
<!-- label contains for attribute and content --> <xs:element id="label" name="label"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="for" type="xs:string"></xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> </xs:schema>
If you think XSD coding is hard, I recommend that you start with writing the XML that you are specifying and then write the XSD to fit that XML. In Visual Studio you will get feedback at once, if your XML does not fit the XSD.
Make sure that you add these files to your project as Embedded Resource, since they're not interesting for anything except your test.
Writing that test
Now we would like to use our schema to validate output of the web control. This is done with the following code.
[TestFixture]
public class UnorderedRadioButtonListShould
{
/* This private class is used to reach protected members in our SUT */
private class Template : ULRadioButtonList
{
public new void Render(HtmlTextWriter htmlTextWriter)
{
base.Render(htmlTextWriter);
}
}
[Test]
public void ProduceExpectedHtmlMarkup()
{
/* Prepare some static variables */
const string Xmlns = "urn:unordered-radio-button-list";
var schema = GetType().Namespace + ".UnorderedRadioButtonList.xsd";
/* Radio button list data source
* <input type="radio" value="Bananas" /><label>I like bananas</label>
* <input type="radio" value="Onions" /><label>I like onions</label>
*/
var data = new ListItemCollection
{
new ListItem("I like bananas", "Bananas"),
new ListItem("I like onions", "Onions")
};
/* SUT */
var control = new Template { DataSource = data };
control.DataBind();
/* Prepare render output stream */
var htmlOutputStream = new MemoryStream();
var htmlOutputStreamWriter = new StreamWriter(htmlOutputStream);
var htmlWriter = new HtmlTextWriter(htmlOutputStreamWriter);
/* Test */
control.Render(htmlWriter);
htmlWriter.Flush();
/* Assert */
htmlOutputStream.Position = 0; /* Reset stream */
/* Load the schema */
var xsdStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(schema);
var xmlSchema = XmlReader.Create(xsdStream);
/* Create xml reader and set schema for validation */
var settings = new XmlReaderSettings();
settings.Schemas.Add(Xmlns, xmlSchema);
settings.ValidationType = ValidationType.Schema;
// Trigger this event on schema validation errors
settings.ValidationEventHandler += HandleValidationErrors;
// Read and validate htmlOutputStream
var xmlValidatingReader = XmlReader.Create(htmlOutputStream, settings);
while (xmlValidatingReader.Read()) ;
}
private void HandleValidationErrors(object sender, ValidationEventArgs e)
{
// There was validation errors, fail with message
Assert.Fail(e.Message);
}
}
}
This looks really cool, but it is completely useless because it will never fail. That is because the HTML is not associated with that schema.
Change the web control
We have to change our system under test to allow this kind of validation. We need to add xmlns="urn:unordered-radio-button-list"
to the root element to apply the validation. But, we don't want this when we render in page. I propose that you add a property called Xmlns and only render its contents on the ul node when it is not null.
Now you will initialize the web control with the following.
Now we have a test that will validate that the web control renders expected markup. Pretty powerful huh! The whole solution can be downloaded from my repository on bitbucket.