As you probably know, the availability of threading in .Net can lead to all kinds of little surprises, together of course with the power to write much more responsive applications.
For instance if you have a WinForms app that spawns a worker thread, and then that worker thread needs to update any UI elements on the main form, it will not work, since you can only update UI elements from the same thread they are created on.
I specifically ran into this when using a custom NUnit TestRunner (see previous post). It turns out, the TestRunner runs the test fixtures on a different thread, so you can't just do a frmMain.txtMessage.Text = "New Text from Test Fixture". Would have been too easy.
In addition, VB.Net also has the concept of default form instances, so when you do the above "frmMain.txtMessage" from a different thread than the main UI thread, VB.Net actually instantiates a new form, and then discards it when you are done. Thus there will be no exception raised for the above code, and putting in a brekpoint in the work thread shows the text properly updated. But in the UI the textbox will be empty.
It is much better explained in this post.
But the gist of it is this:
1. You have to use My.Application.OpenForms to get the correct UI thread instance of your form that you want to update
2. you have to use Invoke/BeginInvoke together with delegates to properly update the controls on the Main UI thread from a different thread.
3. The Control base class has a neat little method called InvokeRequired that will return True if the method is called from a different thread. This way you can write a generic function that works both on the UI thread and from other threads. Something like this:
Public Class frmMain
'some magic voodo that needs to be done here to allow updating of the text box from a different thread
Private Delegate Sub PostMessageDelegate(ByVal strNewMessage As String)
'this is part 2 of the magic voodo: this is a public method that will be called on this form from the
'outside, from different threads, so the text box can be updated
Public Sub PostMessage(ByVal strMessage As String)
If txtMessages.InvokeRequired Then
'this method is called form a different thread, so we need to use BeginInvoke to update the UI
txtMessages.BeginInvoke(New PostMessageDelegate(AddressOf PostMessage), New Object() {strMessage})
Else
txtMessages.Text &= Environment.NewLine & strMessage
End If
End Sub
...
End Class
Tuesday, June 10, 2008
Implement your own NUnit TestRunner - Part 2
After I blogged about creating a basic NUnit TestRunner in Part 1 of this series, this is the follow-up that shows how to extract and display the test results in a simple fashion.
Basically the TestResult object that is being returned from the Run method of the TestRunner contains probably most of the information you need.
Here is the Code:
Imports System.Text
Imports System.Xml
Imports System.IO
Imports System.Reflection
Imports System.Resources
Imports NUnit.Core
Imports NUnit.Util
Public Class Form1
Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click
Dim objTestRunner As TestRunner = Nothing
objTestRunner = New RemoteTestRunner()
Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
Dim objPackage As TestPackage = New TestPackage(strMyFileName)
objTestRunner.Load(objPackage)
'run the test and create the result
Dim objTestResult As TestResult = objTestRunner.Run(New NullListener)
'get the result summary in XML format
Dim strResultSummary As String = CreateXmlOutput(objTestResult)
'now transform the XML into more display-friendly code using the default style sheet
Dim sbResultSummary As New StringBuilder
Dim objXFormReader As XmlTextReader = GetTransformReader(Nothing)
Dim objXMLXform As XmlResultTransform = New XmlResultTransform(objXFormReader)
objXMLXform.Transform(New StringReader(strResultSummary), New StringWriter(sbResultSummary))
txtResults.Text = sbResultSummary.ToString()
End Sub
Private Shared Function CreateXmlOutput(ByVal objResult As TestResult) As String
Dim sbXML As New StringBuilder()
Dim objResultVisitor As New XmlResultVisitor(New StringWriter(sbXML), objResult)
objResult.Accept(objResultVisitor)
objResultVisitor.Write()
Return sbXML.ToString()
End Function 'CreateXmlOutput
Private Shared Function GetTransformReader(ByVal strXSLFileName As String) As XmlTextReader
Dim objXMLReader As XmlTextReader = Nothing
If String.IsNullOrEmpty(strXSLFileName) Then
Dim objNUnitAssembly As Assembly = Assembly.GetAssembly(GetType(XmlResultVisitor))
Dim objResourceMgr As New ResourceManager("NUnit.Util.Transform", objNUnitAssembly)
Dim strXmlData As String = CStr(objResourceMgr.GetObject("Summary.xslt"))
objXMLReader = New XmlTextReader(New StringReader(strXmlData))
Else
Dim objXsltInfo As New FileInfo(strXSLFileName)
If Not objXsltInfo.Exists Then
Throw New FileNotFoundException("Transform file: {0} does not exist", objXsltInfo.FullName)
objXMLReader = Nothing
Else
objXMLReader = New XmlTextReader(objXsltInfo.FullName)
End If
End If
Return objXMLReader
End Function 'GetTransformReader
End Class
this will then show the results in a user-friendly form in the text box txtResults.
Where this comes in handy is if you have a WinForms App and you want to run your Unit tests as part of the WinForms app without having to launch the NUnit GUI (or Console).
This is admittedly a very simple implementation, does not have a lot of the bells and whistles of a more graphical UI, but it tells you which tests failed and how long they took and where they failed.
Basically the TestResult object that is being returned from the Run method of the TestRunner contains probably most of the information you need.
Here is the Code:
Imports System.Text
Imports System.Xml
Imports System.IO
Imports System.Reflection
Imports System.Resources
Imports NUnit.Core
Imports NUnit.Util
Public Class Form1
Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click
Dim objTestRunner As TestRunner = Nothing
objTestRunner = New RemoteTestRunner()
Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
Dim objPackage As TestPackage = New TestPackage(strMyFileName)
objTestRunner.Load(objPackage)
'run the test and create the result
Dim objTestResult As TestResult = objTestRunner.Run(New NullListener)
'get the result summary in XML format
Dim strResultSummary As String = CreateXmlOutput(objTestResult)
'now transform the XML into more display-friendly code using the default style sheet
Dim sbResultSummary As New StringBuilder
Dim objXFormReader As XmlTextReader = GetTransformReader(Nothing)
Dim objXMLXform As XmlResultTransform = New XmlResultTransform(objXFormReader)
objXMLXform.Transform(New StringReader(strResultSummary), New StringWriter(sbResultSummary))
txtResults.Text = sbResultSummary.ToString()
End Sub
Private Shared Function CreateXmlOutput(ByVal objResult As TestResult) As String
Dim sbXML As New StringBuilder()
Dim objResultVisitor As New XmlResultVisitor(New StringWriter(sbXML), objResult)
objResult.Accept(objResultVisitor)
objResultVisitor.Write()
Return sbXML.ToString()
End Function 'CreateXmlOutput
Private Shared Function GetTransformReader(ByVal strXSLFileName As String) As XmlTextReader
Dim objXMLReader As XmlTextReader = Nothing
If String.IsNullOrEmpty(strXSLFileName) Then
Dim objNUnitAssembly As Assembly = Assembly.GetAssembly(GetType(XmlResultVisitor))
Dim objResourceMgr As New ResourceManager("NUnit.Util.Transform", objNUnitAssembly)
Dim strXmlData As String = CStr(objResourceMgr.GetObject("Summary.xslt"))
objXMLReader = New XmlTextReader(New StringReader(strXmlData))
Else
Dim objXsltInfo As New FileInfo(strXSLFileName)
If Not objXsltInfo.Exists Then
Throw New FileNotFoundException("Transform file: {0} does not exist", objXsltInfo.FullName)
objXMLReader = Nothing
Else
objXMLReader = New XmlTextReader(objXsltInfo.FullName)
End If
End If
Return objXMLReader
End Function 'GetTransformReader
End Class
this will then show the results in a user-friendly form in the text box txtResults.
Where this comes in handy is if you have a WinForms App and you want to run your Unit tests as part of the WinForms app without having to launch the NUnit GUI (or Console).
This is admittedly a very simple implementation, does not have a lot of the bells and whistles of a more graphical UI, but it tells you which tests failed and how long they took and where they failed.
Implement your own NUnit TestRunner - Part 1
I wanted to be able to run NUnit unit tests from my own test runner within a WinForms App. Easier said than done, not a lot of examples out there. I looked through the NUnit-Console code and the NUnit-Gui code and pieced together bits and pieces. Then I also stumbled across this blog post from a fellow german which helped with the last stumbling block:
http://achblah.blogspot.com/2007/04/monocov-and-my-contribution-to-it.html
The end result is this piece of VB.net code that will run any tests within your current EXE:
Imports System.Reflection
Imports NUnit.Core
Imports NUnit.Util
Public Class Form1
Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click
Dim objTestRunner As TestRunner = Nothing
objTestRunner = New RemoteTestRunner()
Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
Dim objPackage As TestPackage = New TestPackage(strMyFileName)
objTestRunner.Load(objPackage)
objTestRunner.Run(New NullListener)
End Sub
End Class
Of course this does not show any test results or exceptions because of using the "NullListener". In Part 2 I will fix that.
http://achblah.blogspot.com/2007/04/monocov-and-my-contribution-to-it.html
The end result is this piece of VB.net code that will run any tests within your current EXE:
Imports System.Reflection
Imports NUnit.Core
Imports NUnit.Util
Public Class Form1
Private Sub btnRunTests_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRunTests.Click
Dim objTestRunner As TestRunner = Nothing
objTestRunner = New RemoteTestRunner()
Dim strMyFileName As String = Assembly.GetExecutingAssembly().Location
Dim objPackage As TestPackage = New TestPackage(strMyFileName)
objTestRunner.Load(objPackage)
objTestRunner.Run(New NullListener)
End Sub
End Class
Of course this does not show any test results or exceptions because of using the "NullListener". In Part 2 I will fix that.
Subscribe to:
Posts (Atom)