Monday, 27 July 2009

How we build and test Silverlight

One of my projects at the moment is to write a Silverlight library that calls a web service.

For very good reasons that I won’t go in to right now, we need to build from ant.

We do this by calling the .Net 3.5 c sharp compiler directly.

Here’s a snippet of an ant build.xml file:

<property name="silverlight.path" value="C:/Program Files/Microsoft Silverlight/3.0.40624.0" />
<
property name="silverlight.sdk.path" value="C:/Program Files/Microsoft SDKs/Silverlight/v3.0"
/>

<
target name="compile-source"
>
<
exec dir="src" executable= "C:/Windows/Microsoft.NET/Framework/v3.5/csc.exe" failonerror="true"
>
<
arg line="/optimize"
/>
<
arg line="/debug"
/>
<
arg line="/out:../build/bin/output_file.dll"
/>
<
arg line="/doc:../build/bin/output_file.xml"
/>
<
arg line="/noconfig"
/>
<
arg line="/nostdlib"
/>
<
arg line="/warn:0"
/>
<
arg line="/target:library"
/>
<
arg line="/reference:'${silverlight.path}/mscorlib.dll'"
/>
<
arg line="/reference:'${silverlight.path}/system.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Core.dll'"
/>
<
arg line="/reference:'${silverlight.sdk.path}/Libraries/Client/System.Json.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Net.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Xml.dll'"
/>
<
arg line = "*.cs"
/>
</
exec
>
</
target>

The argument /nostdlib tells the compiler NOT to include any of the base class libraries. This then means that we can pull the Silverlight libraries we need in as reference. We will remove the /debug argument when we are releasing, and sign using a key file too. The final argument merely pulls in all the files from the working directory (in this case we explicitly set it to “src” and compiles these. If you have a non flat directory structure this might need tweaking.

For the testing itself we use this most excellent Silverlight testing framework.

This has perhaps one of the best methods of doing asynchronous testing that I’ve seen. Here’s an example

[TestMethod]
[Asynchronous]
public void MyTestMethod()
{
bool done = false;

ClassUnderTest testObject = new ClassUnderTest ();

EnqueueCallback(() => testObject.TestMethod());
EnqueueConditional(() => done);
EnqueueTestComplete();

testObject.TestMethodComplete += delegate(object sender, OurEventArgs e)
{
try
{
TestResult result = null;
result = e.Data as TestResult;
Assert.AreEqual(0, result.TestProperty);
}
catch (Exception ex)
{
Assert.Fail("Exception - " + ex.Message + " - Stack Trace " + ex.StackTrace);
}
done = true;

};
}

The [Asynchronous] attribute tells the test harness that this method is asynchronous.

All the Enqueue methods set up a series of work items for the test harness to execute asynchronously.

EnqueueConditional(() => done); tells the test harness not to run any more enqueued work items until the done flag has been flipped to true. Which is done in the anonymous method we’ve attached to the TestMethodComplete event.

So the sequence of events is thus

1. Set up a queued lists of callbacks, and wait stages

2. Subscribe to an event – which will be fired asynchronously once the underlying web call completes

3. Wait until that event has fired and then carry on

4. Stop running work items (EnqueueTestComplete)

Now we need to compile the test library and produce something runnable! Back to an ant target.

<target name="compile-tests" depends="create.config">
<
exec dir="test" executable= "C:/Windows/Microsoft.NET/Framework/v3.5/csc.exe" failonerror="true"
>

<
arg line="/optimize"
/>
<
arg line="/noconfig"
/>
<
arg line="/nostdlib"
/>
<
arg line="/warn:0"
/>
<
arg line="/target:library"
/>
<
arg line="/out:../build/bin/output_test_file.dll"
/>
<
arg line="/unsafe"
/>
<
arg line="/reference:'../build/bin/output_file.dll'"
/>
<
arg line="/reference:'lib/Microsoft.Silverlight.Testing.dll'"
/>
<
arg line="/reference:'lib/Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll'"
/>
<
arg line="/reference:'${silverlight.path}/mscorlib.dll'"
/>
<
arg line="/reference:'${silverlight.path}/system.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Core.dll'"
/>
<
arg line="/reference:'${silverlight.sdk.path}/Libraries/Client/System.Json.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Windows.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Windows.Browser.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Net.dll'"
/>
<
arg line="/reference:'${silverlight.path}/System.Xml.dll'"
/>
<
arg line = "*.cs"
/>
</
exec
>

<
copy file="test/AppManifest.xaml" todir="${build}/bin"
/>
<
copy file="C:/Program Files/Microsoft SDKs/Silverlight/v2.0/Libraries/Client/System.Json.dll" todir="${build}/bin"
/>
<
copy file="C:/Program Files/Microsoft SDKs/Silverlight/v2.0/Libraries/Client/System.Xml.Linq.dll" todir="${build}/bin"
/>
<
copy file="test/lib/Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll" todir="${build}/bin"
/>
<
copy file="test/lib/Microsoft.Silverlight.Testing.dll" todir="${build}/bin"
/>
<
zip destfile="${build}/bin/testRunner.xap" basedir="${build}/bin"
/>
<
copy file="test/testRunner.html" todir="${build}/bin"
/>
</target>

This target is complicated by the fact that it creates a .xap file – the one that is sent to the browser. This file is merely a zip file that contains the necessary binaries. and an AppManifest file, which I won’t include here – it’s a trivial file, describing to the run time what the application looks like, it’s entry point etc.

testRunner.html contains the necessary code to download and run testRunner.xap from the web server.

Note that the testRunner.html MUST be launched from a web server, and not from a file uri. We use wamp. The choice of server is irrelevant, it’s serving static files.

One other interesting thing to note is that the service we are calling is on an SSL domain, and the client Silverlight application may well not be. Thus we needed to include on the root of the service domain a clientaccesspolicy file that looks thus:

<?xml version="1.0" encoding="utf-8" ?>
<
access-policy
>
<
cross-domain-access
>
<
policy
>
<
allow-from http-request-headers="*"
>
<
domain uri="http://*"
/>
<
domain uri="https://*"
/>
</
allow-from
>
<
grant-to
>
<
resource include-subpaths="true" path="/"
/>
</
grant-to
>
</
policy
>
</
cross-domain-access
>
</
access-policy>

Those two domain nodes say to the Silverlight runtime not to care if crossdomain calls cross from an insecure domain to our secure domain.

Hope this post helps someone!

No comments:

Tim Stevens

Tim Stevens
Work
Consume
Obey
Be Silent
Die