Thursday, June 05, 2008

Testing Refactorings - Part 2

In this post I will endeavour to explain how to test that your newly crafted refactoring affects your code in manner which you intend it to.

This is going to be a extension of the code supplied in a previous post: How to Test a Refactoring

We are going to add an additional test to our CreateStubForhandler tester to determine if the refactoring operates correctly for a given use case.

For this we have to supply the testing plugin with samples of code which indicate the desired 'before' and 'after' states of the refactoring.

Execute_1.vb looks like this
-------------------------------------------------------------
Namespace Test
    Public Class TestClass
        Public Sub TestMethod()
            Dim X As New System.Windows.Forms.Button
            AddHandler X.Click, AddressOf X_Click<<:caret:>>
        End Sub
    End Class
End Namespace

Namespace Result
    Public Class TestClass
        Public Sub TestMethod()
            Dim X As New System.Windows.Forms.Button
            AddHandler X.Click, AddressOf X_Click
        End Sub

        Private Sub X_Click(ByVal sender As Object, ByVal e As System.EventArgs)
            Throw New System.NotImplementedException()
        End Sub
    End Class
End Namespace
-------------------------------------------------------------
The Build action of this file should be set to Embedded Resource rather than Compile.

image

Then some calling code is necessary.
Initially, I went with the following:
-------------------------------------------------------------
    <Test("Test Execute 1", RESOURCE_PATH, "Execute_1.vb")> _
    Public Sub TestExecute_1(ByVal sender As Object, ByVal ea As TestEventArgs)
        AssertExecute()
    End Sub
-------------------------------------------------------------
But I found that my test would always fail, despite actually producing the correct results in the field (so to speak).

So what was wrong?

I traced into the code as it was running and quickly surmised what was happening.

The original refactoring uses a TargetPicker to allow the user to select a new location for the generated code to appear. The Test had no way of using this UI component to pick a location for the code to go and so effectively the test comparison was done immediately after launching the TargetPicker but before any changes had been made. In fact no changes are made during the course of this test because as things stand, the TargetPicker is never accepted.

So what to do.. Well I figured, let's ask the experts....

So one quick mail to 'da man' (that would be AlexZ) later, and everything became much clearer.

It seems that the big secret to coping with the TargetPicker is quite simple... Turn it off :)

Seriously...The given example was "Extract Method", wherein the user is given a configurable option to either use the picker or have Refactor Insert the extracted Code above or below the source method.

Well unfortunately, CreateStubForHandler has no such option.... So I created one. :) The latest code (revision 81 or better) include just such an option page for this setting.

So now that we have an option that can be set.... We need a way for our Test to set it.

This it turns out is the duty of a new class. DevExpress provide a class 'RefactoringHelper' which we inherit from to create 'CreateStubForHandlerLocalHelper'. It is an instance of this class that we pass to AssertExecute in the following manner.
-------------------------------------------------------------
    <Test("Test Execute 1", RESOURCE_PATH, "Execute_1.vb")> _
    Public Sub TestExecute_1(ByVal sender As Object, ByVal ea As TestEventArgs)
        AssertExecute(New CreateStubForHandlerLocalHelper())
    End Sub
-------------------------------------------------------------

So what goes into 'CreateStubForHandlerLocalHelper' then?

Well given the assumption of a few fields not mentioned, and the following utility code:
-------------------------------------------------------------
    Private Const Section As String = "OptionsCreateStubForHandler"
    Private Const POS_AFTER As String = "After"
    Private Const POS_BEFORE As String = "Before"
    Private Const KEY_DefaultPosition As String = "DefaultPosition"
    Private Const KEY_UseTargetPicker As String = "AllowTargetPicker"

    Private Shared Sub SaveSettings(ByVal Storage As DecoupledStorage, ByVal PositionSetting As String, ByVal AllowUserModifications As Boolean)
        Call Storage.WriteString(Section, KEY_DefaultPosition, PositionSetting)
        Call Storage.WriteBoolean(Section, KEY_UseTargetPicker, AllowUserModifications)
    End Sub

    Private Shared Sub LoadSettings(ByVal Storage As DecoupledStorage, ByRef DefaultPosition As String, ByRef UseTargetPicker As Boolean)
        DefaultPosition = Storage.ReadString(Section, KEY_DefaultPosition, POS_AFTER)
        UseTargetPicker = Storage.ReadBoolean(Section, KEY_UseTargetPicker, False)
    End Sub
-------------------------------------------------------------

All that this class really does, is override the 'UpdateOptions' method and use it to set the options correctly.
-------------------------------------------------------------
    Protected Overrides Sub UpdateOptions()
        MyBase.UpdateOptions()
        Using Storage As DecoupledStorage = GetStorage(PAGE)
            ' Load Existing setting into memory for later.
            Call LoadSettings(Storage, mDefaultPosition, mUseTargetPicker)
            ' Change setting for our own purposes.
            Call SaveSettings(Storage, "After", False)
        End Using
    End Sub
-------------------------------------------------------------
This method is called by the framework prior to the execution of the test, so that the use of the TargetPicker is abandoned and the new code is created "After" the original method just as required.

Et Voila... The test passes.

One final note.... As all good programmers, we really should clean up after ourselves. Shouldn't we?

What do we do about the fact that we just ransacked the main setting of this refactoring just so that we can run the test?

Where/How do we set the setting back to it's original value?

I searched high and I searched low but could not find a RestoreSetting method anywhere on the RefactoringHelper class.

It turns out though that the method we're looking for is called 'AfterExecute'. This method is called after execution of the refactoring but before the comparison.

The main purpose of this method, as I was introduced to it, it to allow you a suitable method in which to "SendKeys" characters into the IDE for testing things like "Rename" where more complex user input is required (see code below).
-------------------------------------------------------------
Protected Overrides Sub AfterExecute()
    CodeRush.Test.SendString(ActiveView.Handle, "SomeCharacters")
End Sub
-------------------------------------------------------------

But it's position within the Test lifecycle makes it a perfect place to restore those settings thus:

-------------------------------------------------------------
    Protected Overrides Sub AfterExecute()
        MyBase.AfterExecute()
        Using Storage As DecoupledStorage = GetStorage(PAGE)
            ' Restore Settings
            Call SaveSettings(Storage, mDefaultPosition, mUseTargetPicker)
        End Using
    End Sub
-------------------------------------------------------------

I am sorry this post was such a long time in coming. I can only claim that things at work are rather busy leaving less than an ideal amount of time for works such as this.

However I will keep going one way or the other.

Happy (plugin) coding.

Please feel free to email any questions to me (RoryBecker [at] Gmail [dot] com) or in the comments to this post.


No comments: