One book that I recommend the reading is Clean Code, by Robert Martin. It is a well written book with wonderful techniques to create better code and improve your current programs, so they become easier to read, maintain and understand.

While going through it again, I found an excellent opportunity to improve my skills trying to do some refactoring: in listing 4.7 there is a prime generator function that he uses to show some refactoring concepts and turn int listing 4.8. I then thought do do the same and show my results here.

We can start with the listing converted to C#. This is a very easy task. The original program is written in  Java, but converting it to C# is just a matter of one or two small fixes:

using System;

namespace PrimeNumbers
{
/**
* This class Generates prime numbers up to a user specified
* maximum. The algorithm used is the Sieve of Eratosthenes.
* <p>
* Eratosthenes of Cyrene, b. c. 276 BC, Cyrene, Libya --
* d. c. 194, Alexandria. The first man to calculate the
* circumference of the Earth. Also known for working on
* calendars with leap years and ran the library at Alexandria.
* <p>
* The algorithm is quite simple. Given an array of integers
* starting at 2. Cross out all multiples of 2. Find the next
* uncrossed integer, and cross out all of its multiples.
* Repeat untilyou have passed the square root of the maximum
* value.
*
* @author Alphonse
* @version 13 Feb 2002 atp
*/
    public class GeneratePrimes
    {
        /**
        * @param maxValue is the generation limit.
*/
        public static int[] generatePrimes(int maxValue)
        {
            if (maxValue >= 2) // the only valid case
            {
                // declarations
                int s = maxValue + 1; // size of array
                bool[] f = new bool[s];
                int i;

                // initialize array to true.
                for (i = 0; i < s; i++)
                    f[i] = true;
                // get rid of known non-primes
                f[0] = f[1] = false;
                // sieve
                int j;
                for (i = 2; i < Math.Sqrt(s) + 1; i++)
                {
                    if (f[i]) // if i is uncrossed, cross its multiples.
                    {
                        for (j = 2 * i; j < s; j += i)
                            f[j] = false; // multiple is not prime
                    }
                }
                // how many primes are there?
                int count = 0;
                for (i = 0; i < s; i++)
                {
                    if (f[i])
                        count++; // bump count.
                }
                int[] primes = new int[count];
                // move the primes into the result
                for (i = 0, j = 0; i < s; i++)
                {
                    if (f[i]) // if prime
                        primes[j++] = i;
                }
                return primes; // return the primes
            }
            else // maxValue < 2
                return new int[0]; // return null array if bad input.
        }
    }
}

The first step is to put in place some tests, so we can be sure that we are not breaking anything while refactoring the code. In the solution, I added a new Class Library project, named it GeneratePrimes.Tests and added the packages NUnit, NUnit3TestAdapter and FluentAssertions to get fluent assertions in a NUnit test project. Then I added these tests:

using NUnit.Framework;
using FluentAssertions;

namespace PrimeNumbers.Tests
{
    [TestFixture]
    public class GeneratePrimesTests
    {
        [Test]
        public void GeneratePrimes0ReturnsEmptyArray()
        {
            var actual = GeneratePrimes.generatePrimes(0);
            actual.Should().BeEmpty();
        }

        [Test]
        public void GeneratePrimes1ReturnsEmptyArray()
        {
            var actual = GeneratePrimes.generatePrimes(1);
            actual.Should().BeEmpty();
        }

        [Test]
        public void GeneratePrimes2ReturnsArrayWith2()
        {
            var actual = GeneratePrimes.generatePrimes(2);
            actual.Should().BeEquivalentTo(new[] { 2 });
        }

        [Test]
        public void GeneratePrimes10ReturnsArray()
        {
            var actual = GeneratePrimes.generatePrimes(10);
            actual.Should().BeEquivalentTo(new[] { 2,3,5,7 });
        }

        [Test]
        public void GeneratePrimes10000ReturnsArray()
        {
            var actual = GeneratePrimes.generatePrimes(10000);
            actual.Should().HaveCount(1229).And.EndWith(9973);
        }
    }
}

These tests check that there are no primes for 0 and 1, one prime for 2, the primes for 10 are 2, 3, 5, 7 and that there are 1229 primes less than 10,000 and the largest one is 9973. Once we run the tests, we can see that the pass and we can start doing our changes.

The easiest fix we can do is to revise the comments at the beginning. We don’t need the history of Erasthotenes (you can go to Wikipedia for that). We don’t need the author and version, thanks to source control technology :-). We don’t need either the initial comment:

/**
    * This class Generates prime numbers up to a user specified
    * maximum. The algorithm used is the Sieve of Eratosthenes.
    *  https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes   
*/
public class GeneratePrimes
{
    public static int[] generatePrimes(int maxValue)

Then we can invert the initial test, to reduce nesting. If we hover the mouse in the line of the first if, an arrow appears at the border, indicating a quick fix:

We can do the quick fix, then eliminate the else clause (don’t forget to remove the extra comments that are not needed):

public static int[] generatePrimes(int maxValue)
{
    if (maxValue < 2) 
        return new int[0]; 

    // declarations
    int s = maxValue + 1; // size of array
    bool[] f = new bool[s];
    int i;

Save the code and check that all tests pass. The next step is to rename the variables:

  • s can be renamed to sizeOfArray
  • f can be renamed as isPrimeArray

Go to the declaration of s and press Ctrl-R-R to rename and rename it to sizeOfArray. Do the same with the f variable. Don’t forget to remove the comments (and to run the tests):

int sizeOfArray = maxValue + 1; 
bool[] isPrimeArray = new bool[sizeOfArray];
int i;

To go to the next refactorings, we can use the comments as indicators for extracting methods. We can extract the InitializeArray method:

The extracted code isn’t what I expected, so I change it to:

private static bool[] InitializeArray(int sizeOfArray)
{
    bool[] isPrimeArray = new bool[sizeOfArray];
    // initialize array to true.
    for (var i = 0; i < sizeOfArray; i++)
        isPrimeArray[i] = true;
    return isPrimeArray;
}

I can use the code like this:

var isPrimeArray = InitializeArray(sizeOfArray);

After passing the tests, I can refactor the code of InitializeArray to:

private static bool[] InitializeArray(int sizeOfArray)
{
    return Enumerable
        .Range(0, sizeOfArray)
        .Select(n => true)
        .ToArray();
}

The next step is the sieve:

The code for the sieve is really bad:

private static void Sieve(int sizeOfArray, bool[] isPrimeArray, 
    out int i, out int j)
{
    // get rid of known non-primes
    isPrimeArray[0] = isPrimeArray[1] = false;
    for (i = 2; i < Math.Sqrt(sizeOfArray) + 1; i++)
    {
        if (isPrimeArray[i]) // if i is uncrossed, cross its multiples.
        {
            for (j = 2 * i; j < sizeOfArray; j += i)
                isPrimeArray[j] = false; // multiple is not prime
        }
    }
}

It has two out parameters (which, for me, is a code smell), and has an error (the out parameter j must be assigned) before exiting the method. So we can change it to remove the out parameters and remove the sizeOfArray parameter:

private static void Sieve(bool[] isPrimeArray)
{
    var sizeOfArray = isPrimeArray.Length;

    isPrimeArray[0] = isPrimeArray[1] = false;

    for (int i = 2; i < Math.Sqrt(sizeOfArray) + 1; i++)
    {
        if (isPrimeArray[i]) // if i is uncrossed, cross its multiples.
        {
            for (int j = 2 * i; j < sizeOfArray; j += i)
                isPrimeArray[j] = false; 
        }
    }

Then, we can extract the method to count primes:

CountPrimes has the same flaws as Sieve, so we change it to:

private static int CountPrimes(bool[] isPrimeArray)
{
    var sizeOfArray = isPrimeArray.Length;
    var count = 0;
    for (var i = 0; i < sizeOfArray; i++)
    {
        if (isPrimeArray[i])
            count++; 
    }
    return count;
}

We can refactor it to:

private static int CountPrimes(bool[] isPrimeArray) => 
    isPrimeArray.Count(i => i);

The next step is MovePrimes:

After we tweak the MovePrimes code, we get:

private static int[] MovePrimes(bool[] isPrimeArray, int count)
{
    var sizeOfArray = isPrimeArray.Length;
    var primes = new int[count];
    for (int i = 0, j = 0; i < sizeOfArray; i++)
    {
        if (isPrimeArray[i]) // if prime
            primes[j++] = i;
    }
    return primes;
}

Then we can refactor MovePrimes:

 private static int[] MovePrimes(bool[] isPrimeArray, int count) =>
     isPrimeArray
         .Select((p, i) => new { Index = i, IsPrime = p })
         .Where(v => v.IsPrime)
         .Select(v => v.Index)
         .ToArray();

Notice that we aren’t using the primes count in this case, so we can remove the calculation of the count and the parameter. After some cleaning and name changing, we get:

public static int[] GetPrimes(int maxValue)
{
    if (maxValue < 2)
        return new int[0];

    bool[] isPrimeArray = InitializeArray(maxValue);
    Sieve(isPrimeArray);
    return MovePrimes(isPrimeArray);
}

Much cleaner, no? Now, it’s easier to read the method, the details are hidden, but the code still runs the same way. We have a more maintainable method, and it shows clearly what it does.

But there is a change we can do here: we are using static methods only. We can then use extension methods and add the keyword this to allow the methods to be used as extension methods. For example, if we change MovePrimes and Sieve to:

private static int[] MovePrimes(this bool[] isPrimeArray) =>
    isPrimeArray
        .Select((p, i) => new { Index = i, IsPrime = p })
        .Where(v => v.IsPrime)
        .Select(v => v.Index)
        .ToArray();

private static bool[] Sieve(this bool[] isPrimeArray)
{
    var sizeOfArray = isPrimeArray.Length;

    isPrimeArray[0] = isPrimeArray[1] = false;

    for (int i = 2; i < Math.Sqrt(sizeOfArray) + 1; i++)
    {
        if (isPrimeArray[i]) // if i is uncrossed, cross its multiples.
        {
            for (int j = 2 * i; j < sizeOfArray; j += i)
                isPrimeArray[j] = false;
        }
    }
    return isPrimeArray;

We can have the GetPrimes method to be changed to:

public static int[] PrimesSmallerOrEqual(this int maxValue)
{
    if (maxValue < 2)
        return new int[0];

    return maxValue.InitializeArray()
        .Sieve()
        .MovePrimes();
}

Cool, no? With this change, the tests become:

public class GeneratePrimesTests
{
    [Test]
    public void GeneratePrimes0ReturnsEmptyArray()
    {
        0.PrimesSmallerOrEqual().Should().BeEmpty();
    }

    [Test]
    public void GeneratePrimes1ReturnsEmptyArray()
    {
        1.PrimesSmallerOrEqual().Should().BeEmpty();
    }

    [Test]
    public void GeneratePrimes2ReturnsArrayWith2()
    {
        2.PrimesSmallerOrEqual()
            .Should().BeEquivalentTo(new[] { 2 });
    }

    [Test]
    public void GeneratePrimes10ReturnsArray()
    {
        10.PrimesSmallerOrEqual()
            .Should().BeEquivalentTo(new[] { 2, 3, 5, 7 });
    }

    [Test]
    public void GeneratePrimes10000ReturnsArray()
    {
        10000.PrimesSmallerOrEqual()
            .Should().HaveCount(1229).And.EndWith(9973);
    }
}

The full code is at https://github.com/bsonnino/PrimeNumbers. Each commit there is a phase of the refactoring.

While writing my last article, something occurred to me: what if the app uses an external dll for some functions, how can I package them to send them to the store. Once you are sending an app to the Windows Store, everything it needs to start and run must be packaged, or it won’t be certified.

When using an installer to package an app, all you must do is to include the main executable and all the needed files in the install script and the installer will take care of packaging everything. But when your are packaging an app for the Windows Store, there is no such thing as an install script. If you are packaging a Delphi app, just compile it to the Store and voilà!, an appx file is created. With Visual Studio, you can create a packaging project, add all the projects in the solution that you want and it will create an appx file for you.

But sometimes, you need to use an external dll, which you may not have the source code, and it must be packaged with the main executable. In this article, I will show how to package an external dll with the main executable with Delphi and with Visual Studio.

For the article, we will take on the Financial Calculator that we created for the last article and refactor it: the financial functions will be in an external Win32 dll and the UI will be in the main executable. That way, we will do two things:

  • Separate the UI and the business rules
  • Create an external component that may be used in many situations: we will use it for our two apps – the Delphi and the WPF one. That is a great way to refactor your code when you have stable business rules that you don’t want to touch and you need to evolve the UI of your app

Refactoring the Delphi UI

As you can see from this code, the business rules are mixed with the UI, thus making it difficult to understand it and change the code, if it’s needed.

procedure TForm1.CalculatePV;
begin
  try
    var FutureValue := StrToFloat(FvPresentValue.Text);
    var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpPresentValue.Text);
    var PresentValue := FutureValue / Power((1 + InterestRate), NumPeriods);
    PvPresentValue.Text := FormatFloat('0.00', PresentValue);
  except
    On EConvertError do
      PvPresentValue.Text := '';
  end;
end;

For example, all the values are dependent on the text box values and the result is also posted in the result text box. This is bad design and not testable. A better thing would be something like this:

function TForm1.CalculatePV(FutureValue, InterestRate : Double; NumPeriods : Integer); double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

This is cleaner, does not depend on the UI and easier to understand. But it is not testable, yet, because the method is in the code behind for the UI, so to test it you should need to instantiate a new Form1, which is not feasible under automated tests (unless you are doing UI tests, which is not the case). You could move this code to another unit, to allow it to be testable, but it won’t be reusable. If you want to use the same code in another program, you should have to copy the unit, with all the problems you may have with that:

  • Difficulty to change the code: if you find an error or want to refactor the code, you should fix the same thing in many places
  • Impossible to use in programs written in other languages, unless you rewrite the code

The best way in this case is to move the code to an external dll. That way, the code will be both testable and reusable: you can even use the same dll in programs written in other languages with no change.

The first step is to create a new project in the project group, a dynamic library. Save it and call it FinCalcDll. Then add a new unit to it and save it as PVCalculator. You should be asking why am I saving the unit with this name and not as FinancialCalculators. I am doing this because I want to treat each unit as a single class and respect the Single Responsibility Principle. Following that principle, the class should have only one reason to change. If I put all calculators in a single unit (class), there will be more than one reason to change it: any change in any of the calculators will be a reason to change. Then, we can add the first function:

unit PVCalculator;

interface

Uses Math;

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer): double; stdcall;

implementation

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.

We must use the Math unit, to have the Power function available and declare the function in the Interface section, so it can be visible externally. It must be declared as stdcall to be called by other languages. Create new units and save them as FVCalculator, IRRCalculator and PmtCalculator and add these functions:

unit PVCalculator;

interface

Uses Math;

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double; stdcall;

implementation

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.
unit FVCalculator;

interface

Uses Math;

function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  try
    Result := PresentValue * Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.
unit IRRCalculator;

interface

Uses Math;

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
  Double;
begin
  Result := Nan;
  try
    var FoundRate := False;
    var MinRate := 0.0;
    var MaxRate := 1.0;
    if Payment * NumPeriods < PresentValue then begin
      Result := -1;
      exit;
    end;
    if Payment * NumPeriods = PresentValue then begin
      Result := 0;
      exit;
    end;
    while not FoundRate do begin
      var Rate := (MaxRate + MinRate) / 2.0;
      var SumPayments := 0.0;
      for var I := 1 to NumPeriods do
        SumPayments := SumPayments + Payment / Power((1 + Rate), I);
      if Abs(SumPayments - PresentValue) > 0.01 then begin
        if PresentValue < SumPayments then begin
          MinRate := Rate;
        end
        else begin
          MaxRate := Rate;
        end;
      end
      else begin
        FoundRate := True;
        Result := Rate;
      end;
    end;
  except
  end;
end;

end.<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1">​</span>
unit PmtCalculator;

interface

Uses Math;

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  try
    Result := (PresentValue * InterestRate) * Power((1 + InterestRate),
      NumPeriods) / (Power((1 + InterestRate), NumPeriods) - 1);
  except
    Result := Nan;
  end;
end;

end.

In the dpr file, you must export the functions. In the Projects window, select the dll and right-click on it, selecting the View Source option. In the source for the dpr file, add the Exports clause:

{$R *.res}
Exports
  CalculatePV, CalculateFV, CalculateIRR, CalculatePmt;

Then, in the Unit1 for the executable, make the changes needed to use the new dll functions:

implementation

{$R *.dfm}
function CalculatePV(FutureValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculateFV(PresentValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculatePmt(PresentValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

procedure TForm1.PaymentChange(Sender: TObject);
begin
  try
    var PresentValue := StrToFloat(PvPayment.Text);
    var InterestRate := StrToFloat(IRPayment.Text) / 100.0;
    var NumPayments := StrToInt(NpPayment.Text);
    var Payment := CalculatePmt(PresentValue,InterestRate, NumPayments);
    PmtPayment.Text := FormatFloat('0.00', Payment);
  except
    On EConvertError do
      PmtPayment.Text := '';
  end;
end;

procedure TForm1.PresentValueChange(Sender: TObject);
begin
  try
    var FutureValue := StrToFloat(FvPresentValue.Text);
    var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpPresentValue.Text);
    var PresentValue := CalculatePV(FutureValue, InterestRate, NumPeriods);
    if IsNan(PresentValue) then
      PvPresentValue.Text := ''
    else
      PvPresentValue.Text := FormatFloat('0.00', PresentValue);
  except
    On EConvertError do
      PvPresentValue.Text := '';
  end;
end;

procedure TForm1.IRRChange(Sender: TObject);
begin
  try
    var NumPayments := StrToInt(NpIRR.Text);
    var PresentValue := StrToFloat(PvIRR.Text);
    var Payment := StrToFloat(PmtIRR.Text);
    var Rate := CalculateIRR(PresentValue, Payment, NumPayments);
    if Rate < 0 then begin
      IRIRR.Text := 'Rate Less than 0';
      exit;
    end;
    if IsNan(Rate) then begin
      IRIRR.Text := 'Error calculating rate';
      exit;
    end;
    IRIRR.Text := FormatFloat('0.00', Rate * 100.0);
  except
    On EConvertError do
      IRIRR.Text := '';
  end;
end;

procedure TForm1.FutureValueChange(Sender: TObject);
begin
  try
    var PresentValue := StrToFloat(PvFutureValue.Text);
    var InterestRate := StrToFloat(IrFutureValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpFutureValue.Text);
    var FutureValue := CalculateFV(PresentValue,InterestRate, NumPeriods);
    if IsNan(FutureValue) then
      FvFutureValue.Text := ''
    else
      FvFutureValue.Text := FormatFloat('0.00', FutureValue);
  except
    On EConvertError do
      FvFutureValue.Text := '';
  end;
end;

We declare the functions and use them in the OnChange handlers of the textboxes. When you build and run the program, you will see something like this:

That’s because the dll is not where it should be, in the same folder of the executable. For that, you must take some steps:

  • Build the dll before the executable. If you don’t do that, the executable will be built and will use an outdated dll
  • Copy the dll after building the executable

For the first step, you need to go to the Projects window, select the dll, right click and select the “Build Sooner”  option. That will move the dll up in the project list and will make it to be built before the executable.

For the second step, you need to add a post-build step for the executable and copy the dll to the output dir. For that, you need to select the Project Options and go to Build Events:

There, in the Post-build events, you should add a command to copy the dll to the executable output dir:

One thing must be noted here: you must build the dll and the executable for the same platform. If you build the dll for x64, it won’t run on a x86 executable. Once you’ve done the two steps, you can build all projects and run the executable, it will run the same way as before. Now, we’ve refactored all the business rules into a dll and we can reuse it in other languages. To show that, we will create a WPF project in C# that will use this dll.

Creating a WPF project that uses the DLL

Go to Visual Studio and create a new WPF project, and name it FinCalcWPF. Then go to solution explorer and add the dll file to the project. When adding the dll, select Add as Link, to avoid to make a physical copy of the dll in the source directory. This way, you are just adding a link to the dll and when it’s rebuilt, the new version will be used. In the properties window, select Build Action to None and Copy to Output Directory to Copy if newer.

One thing must be noted here: the dll is for Win32, so the executable should also be for Win32. When you build the WPF app with the default setting (Any CPU), you can’t be sure it if will run as a Win32 process:

  • For a Win32 operating system, it will run as a Win32 process, so that’s ok
  • For a Win64 operating system, it may run as a Win32 or Win64 process, depending on your settings. If you go to Project/Options/Build and check the “Prefer 32-bit”, it will run as a Win32 process, else it will run as a Win64 process

So, if you don’t want any surprises, just change the build from Any CPU to x86 and you will be sure that the program will run with the dll.

Then, in MainWindow.xaml, add this code:

<Window x:Class="FinCalcWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="Financial Calulator WPF" Height="293.774" Width="419.623">
    <Grid>
        <TabControl>
            <TabItem Header="Present Value">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                             Margin="5" Text="Future Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Present Value"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                            x:Name="PvFvBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvIrBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvNpBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvPvBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Future Value">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Future Value"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvPvBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvIrBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvNpBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvFvBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Payment">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Payment"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtPvBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtIrBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtNpBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtPmtBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Return Rate">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Payment"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Return Rate"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrPvBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrPmtBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrNpBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrRrBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

We are adding the four tabs with the boxes, the same way we’ve added in the Delphi app. The code behind for the window is:

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;

namespace FinCalcWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        [DllImport("FinCalcDll.dll")]
        private static extern double CalculatePV(double futureValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculateFV(double presentValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculatePmt(double presentValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculateIRR(double presentValue, double payment, int numPeriods);

        public MainWindow()
        {
            InitializeComponent();
        }

        private void PvOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(PvFvBox.Text, out double futureValue) &&
                double.TryParse(PvIrBox.Text, out double interestRate) &&
                int.TryParse(PvNpBox.Text, out int numPeriods))
              PvPvBox.Text = CalculatePV(futureValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void FvOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(FvPvBox.Text, out double presentValue) &&
                double.TryParse(FvIrBox.Text, out double interestRate) &&
                int.TryParse(FvNpBox.Text, out int numPeriods))
                FvFvBox.Text = CalculateFV(presentValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void PmtOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(PmtPvBox.Text, out double presentValue) &&
                double.TryParse(PmtIrBox.Text, out double interestRate) &&
                int.TryParse(PmtNpBox.Text, out int numPeriods))
                PmtPmtBox.Text = CalculatePmt(presentValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void RrOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(RrPvBox.Text, out double presentValue) &&
                double.TryParse(RrPmtBox.Text, out double payment) &&
                int.TryParse(RrNpBox.Text, out int numPeriods))
                RrRrBox.Text = (CalculateIRR(presentValue, payment, numPeriods)*100.0).ToString("N2");
        }
    }
}

We’ve declared the functions in the dll and the we used them in the TextChanged event handlers. That will fill the result boxes in the tabs when you fill the input boxes. When you run the program, you will have the same result in both apps:

As you can see, refactoring the code into a dll brings many advantages: the code is not dependent on the UI, it is reusable and, best of all, it is testable. Creating unit tests for the code is a great way to be sure that everything works fine and, if you are making a change, you haven’t introduced a bug. Now, we’ll add the tests for the dll functions.

Adding tests to the dll

To add tests to the dll we must create a new test project to the group. Right click on the Project group and select “Add new project”. Then, select the DUnitX project, and set its settings:

When you click the OK button, Delphi will create a new test project with an unit with sample tests. You need to add the four calculator units to your new project and then, we can create the first test:

unit PVCalculatorTests;

interface
uses
  DUnitX.TestFramework, PVCalculator, Math;

type

  [TestFixture]
  TPvCalculatorTests = class(TObject)
  public
    [Test]
    [TestCase('FutureValue','-1,0,0')]
    [TestCase('Rate','0,-1,0')]
    [TestCase('Periods','0,0,-1')]
    procedure NegativeInputParametersReturnNan(const FutureValue : Double;
      const Rate : Double; const Periods : Integer);
  end;

implementation

procedure TPvCalculatorTests.NegativeInputParametersReturnNan(const FutureValue,
  Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.IsTrue(IsNan(result));
end;

initialization
  TDUnitX.RegisterTestFixture(TPvCalculatorTests);
end.

We named the unit PvCalculatorTests. Then we add the PVCalculator and Math units to the Uses clause. Then, we set the [TextFixture] attribute to the test class, to tell the test framework that this is a class that will have tests. Then we create a method and decorate it with the [Test] attribute. As this will be a parametrized test, we add the cases with the TestCase attribute.

The test is simple. We will run the test with the parameters (there will always be a negative parameter) and the result must always be NaN, thus pointing an invalid entry. If you run the project you will see something like this:

As you can see, the generated test project is a console app that runs the tests and shows the results. If you want a GUI app for the tests, you should install the TestInsight IDE plugin. As you can see from the image, all tests failed, because we have not checked the input parameters. We can change that in PVCalculator:

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  if (FutureValue < 0) or (InterestRate < 0) or (NumPeriods < 0) then begin
    Result := NaN;
    exit;
  end;
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NaN;
  end;
end;

Now, when you run the tests, all pass:

Now, we can create more tests for this calculator:

unit PVCalculatorTests;

interface
uses
  DUnitX.TestFramework, PVCalculator, Math;

type

  [TestFixture]
  TPvCalculatorTests = class(TObject)
  public
    [Test]
    [TestCase('FutureValue','-1,0,0')]
    [TestCase('Rate','0,-1,0')]
    [TestCase('Periods','0,0,-1')]
    procedure NegativeInputParametersReturnNan(const FutureValue : Double;
      const Rate : Double; const Periods : Integer);

    [Test]
    [TestCase('OnePeriod','100,1')]
    [TestCase('TenPeriods','100,10')]
    [TestCase('OneHundredPeriods','100,100')]
    procedure ZeroRatePresentValueEqualsFutureValue(const FutureValue : Double;
      const Periods : Integer);

    [Test]
    [TestCase('OnePeriodOnePercent','0.01,1')]
    [TestCase('OnePeriodTenPercent','0.10,1')]
    [TestCase('OnePeriodHundredPercent','1.00,1')]
    [TestCase('TenPeriodOnePercent','0.01,10')]
    [TestCase('TenPeriodTenPercent','0.10,10')]
    [TestCase('TenPeriodHundredPercent','1.00,10')]
    [TestCase('HundredPeriodOnePercent','0.01,100')]
    [TestCase('HundredPeriodTenPercent','0.10,100')]
    [TestCase('HundredPeriodHundredPercent','1.00,100')]
    procedure ZeroFutureValueEqualsZeroPresentValue(const Rate : Double;
      const Periods : Integer);
      
    [Test]
    [TestCase('OnePeriodOnePercent','100,0.01,1,99.01')]
    [TestCase('OnePeriodTenPercent','100,0.10,1,90.91')]
    [TestCase('OnePeriodHundredPercent','100,1.00,1,50')]
    [TestCase('TenPeriodOnePercent','100,0.01,10,90.53')]
    [TestCase('TenPeriodTenPercent','100,0.10,10,38.55')]
    [TestCase('TenPeriodHundredPercent','100,1.00,10,0.10')]
    [TestCase('HundredPeriodOnePercent','100,0.01,100,36.97')]
    [TestCase('HundredPeriodTenPercent','100,0.10,100,0.01')]
    [TestCase('HundredPeriodHundredPercent','100,1.00,100,0.00')]
    procedure VariablePeriodTests(const FutureValue : Double;
      const Rate : Double; const Periods : Integer; const Expected : Double);

  end;

implementation

procedure TPvCalculatorTests.NegativeInputParametersReturnNan(const FutureValue,
  Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.IsTrue(IsNan(result));
end;

procedure TPvCalculatorTests.VariablePeriodTests(const FutureValue, Rate: Double;
  const Periods: Integer; const Expected: Double);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.AreEqual(Expected,Double(result));
end;

procedure TPvCalculatorTests.ZeroFutureValueEqualsZeroPresentValue(
  const Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(0,Rate,Periods);
  Assert.AreEqual(Double(0.0),Double(result));
end;

procedure TPvCalculatorTests.ZeroRatePresentValueEqualsFutureValue(
  const FutureValue Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,0,Periods);
  Assert.AreEqual(FutureValue,Double(result));
end;

initialization
  TDUnitX.RegisterTestFixture(TPvCalculatorTests);
end.

You should note one thing. When you run the tests, you will see that some of them fail:

This is not a failure in our code, but a failure in the test. As we are comparing double values, there are many decimals to compare and that’s not what you want. You can change your test to compare the difference to a maximum value. If the difference is greater than the maximum, the test fails:

procedure TPvCalculatorTests.VariablePeriodTests(const FutureValue, Rate: Double;
  const Periods: Integer; const Expected: Double);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.AreEqual(Expected,Double(result), 0.01);
end;

Now, all tests pass. You can create tests for the other calculators the same way we did for this one. If you are using Delphi Rio and run the tests with debugging, you will see that some tests give a floating point error:

procedure TPmtCalculatorTests.ZeroPeriodsValueEqualsPresentValue(
  const PresentValue, Rate: Double);
begin
  var result := CalculatePmt(PresentValue,Rate, 0);
  Assert.AreEqual(Double(PresentValue),Double(result),0.01);
end;

procedure TPmtCalculatorTests.ZeroRatePmtEqualsPresentValueDivPeriods(
  const PresentValue: Double; const Periods: Integer);
begin
  var result := CalculatePmt(PresentValue,0,Periods);
  Assert.AreEqual(Double(PresentValue/Periods),Double(result),0.01);
end;

But the tests still pass. That’s because there is a bug in Delphi Rio (QC#RSP-19882), where comparisons with NaN return true, while they should return false. This can be solved by changing the tests to:

procedure TPmtCalculatorTests.ZeroPeriodsValueEqualsPresentValue(
  const PresentValue, Rate: Double);
begin
  var result := CalculatePmt(PresentValue,Rate, 0);
  Assert.IsFalse(IsNan(Result));
  Assert.AreEqual(Double(PresentValue),Double(result),0.01);
end;

procedure TPmtCalculatorTests.ZeroRatePmtEqualsPresentValueDivPeriods(
  const PresentValue: Double; const Periods: Integer);
begin
  var result := CalculatePmt(PresentValue,0,Periods);
  Assert.IsFalse(IsNan(Result));
  Assert.AreEqual(Double(PresentValue/Periods),Double(result),0.01);
end;

When you run the tests again, you will see that they will fail. We must change the calculator to solve this:

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  if (PresentValue < 0) or (InterestRate < 0) or (NumPeriods < 0) then begin
    Result := NaN;
    exit;
  end;
  try
    if InterestRate = 0 then
      Result := PresentValue/NumPeriods
    else if NumPeriods = 0 then
      Result := PresentValue
    else
      Result := (PresentValue * InterestRate) * Power((1 + InterestRate),
        NumPeriods) / (Power((1 + InterestRate), NumPeriods) - 1);
  except
    Result := Nan;
  end;
end;

After this, our dll and its tests are ready to use and can be used in any language that supports Win32 dlls.

Conclusions

We’ve come a long way from the calculator code mixed with the UI to a new dll with unit tests. This architecture is more robust, reusable and easier to maintain. If we need to make changes to the dll, we are covered by unit tests, that can assure we are not introducing new bugs. And if some bug is found in the functions, it’s just a matter of writing a new test that fails, thus making sure of the bug, fix the code and rerun the test, making sure it’s passed. Using the Red-Refactor-Green procedure, we have a safety net for changing our code.

The full code for this article is at https://github.com/bsonnino/FinCalcDll

You have finished you application, and you have created a lot of unit tests, it should be ok, no? Not quite. There are a lot of issues that can arise from the UI. You cannot unit test the UI and, if you done it right, you have a full set of tests for your Model and ViewModel, but not for the View.

For web applications, you can test the UI with something like Selenium (http://www.seleniumhq.org/), a framework that automates browsers and can test the UI of your web apps. You can even check my article (in portuguese) here, where I show how to automate a web search and store the results in a WPF application.

But and what can be done for desktop applications? You can use UI automation and create a Coded UI Test (https://msdn.microsoft.com/en-us/library/dd286726.aspx), but that only works with Visual Studio Enterprise. You could also use something like UI Automation PowerShell Extensions, but this is somewhat cumbersome: you must program your tests using PowersSell and test the results – there is no test runner, except the PowerShell window.

Wouldn’t it be good to create our tests the same way we create our unit tests, using the test framework we are used to (that can be MS-Test, NUnit, XUnit, and so on?). That’s not out of reality. Now, there is something available for us: Appium (http://appium.io/).

Appium is an open source framework that uses the same technology as Selenium to test Android and iOS apps, but Microsoft has created a WebDriver for Appium that allows you to test also Windows apps. With it, you can test any Windows app, and it can be a Win32 app, .NET app or even Windows UWP apps. And the best thing is that you can youse any test framework you want to create your tests. Nice, no? And did I say that you don’t need to have the app’s source code to create the tests?

Installing the Microsoft WinDriver

To install the Microsoft WinDriver, you can go to https://github.com/Microsoft/WinAppDriver/releases and download WindowsApplicationDriver.msi and run it. Once it is installed, just go to the installation path and run WinAppDriver.exe and that’s it. You are up and running.

The web server is listening at http://127.0.0.1:4723. You can test this by opening a web browser window and typing the address: http://127.0.0.1:4723/status. You should get something like this:

{"build":{"revision":"30002","time":"Wed Nov 30 16:48:11 2016","version":"0.7.1611"},"os":{"arch":"amd64","name":"windows","version":"10"}}

 

Now we are ready to create our first tests. To show you I don’t need any source code, we will create tests for the Windows Calculator.

Testing the Windows Calculator

Open Visual Studio and create a new Test Project for the .NET Framework and name it CalcTests. Then right-click in the References node in the Solution Explorer and select “Manage NuGet packages”. Select the Appium.WebDriver.

Then let’s create our first test. In TestMethod1 method in UnitTest1.cs, type the following:

public void TestMethod1() 
{
    DesiredCapabilities appCapabilities = new DesiredCapabilities();
    appCapabilities.SetCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
    appCapabilities.SetCapability("deviceName", "WindowsPC");
    var calculatorSession = new WindowsDriver(new Uri("http://127.0.0.1:4723"), appCapabilities);
    Assert.IsNotNull(calculatorSession);
}

Then run it. If everything goes fine, the test should pass and the calculator should be open in your desktop. If you read the code, you may ask: “where do I find this Id in the second line of the method?”. If you designed the app, you can find it in the app manifest, in the Packaging tab:

image

If you haven’t designed it, you can check your installed packages with PowerShell with a command like this:

Get-AppxPackage | where {$_.Name -like "*calc*"}

 

And get the family name:

image

With this, we can interact with our tests. In order to don’t have to open the calculator after each test, we can create a method to initialize the calculator at the beginning of the tests and close it at the end:

public class CalculatorTests
{
    private static WindowsDriver _calculatorSession;
    
    [TestMethod]
    public void CalculatorIsNotNull()
    {
        Assert.IsNotNull(_calculatorSession);
    }
    
    [ClassInitialize]
    public static void StartCalculator(TestContext context)
    {
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
        appCapabilities.SetCapability("deviceName", "WindowsPC");
        _calculatorSession = new WindowsDriver(new Uri("http://127.0.0.1:4723"), appCapabilities);
    }
    
    [ClassCleanup]
    public static void CloseCalculator()
    {
        _calculatorSession.Dispose();
        _calculatorSession = null;
    }
}

The next step is to be sure that the calculator is in a know state at the start of the test. This is done in the method marked with the TestInitialize attribute:

[TestInitialize]
public void ResetCalculator()
{
  _calculatorSession.FindElementByAccessibilityId("NavButton").Click();
  _calculatorSession.FindElementByName("Standard Calculator").Click();
  _calculatorSession.FindElementByName("Clear").Click();
}

This code clicks the navigator button and then selects the “Standard Calculator” menu item and clears the display. As you can see from the code, I am using the FindElementByAccessibilityId and FindElementByName. You should be asking yourself “And where do I find these Ids”. You can inspect all apps running in your machine using the Inspect program that comes with the SDK. Open a Visual Studio Command Prompt and type inspect. A program like this should open:

image

You can inspect the calculator and get the Ids and names for the buttons and menu items. Note that in some cases (like in the CalculatorResults textbox), the Name property changes and you cannot find it by name, so you must use the AccessibilityId, that doesn’t change.

With that, we can create our second test, that will ensure that clicking in a key will display the corresponding number in the results. For this test, I will use a new feature introduced in MSTest V2, Data Row Tests – you can create a single method that will run different tests with the data entries you specify. This is different that using many assertions in the same test: when you use a Data Row Test, you have independent tests for each row, and you can see which ones fail (and the TestInitialize and TestCleanup are run for each one). When you join all assertions in a single test, if one fails, the remaining ones are not run, so you don’t know if one of the following tests also fails. And, besides that, the TestInitialize and TestCleanup are not run for each assertion.

[DataTestMethod]
[DataRow("One", "1")]
[DataRow("Two", "2")]
[DataRow("Three", "3")]
[DataRow("Four", "4")]
[DataRow("Five", "5")]
[DataRow("Six", "6")]
[DataRow("Seven", "7")]
[DataRow("Eight", "8")]
[DataRow("Nine", "9")]
[DataRow("Zero", "0")]
public void CalculatorKeysShouldDisplay(string key, string expected)
{
  _calculatorSession.FindElementByName(key).Click();
  var actual = _calculatorSession.FindElementByAccessibilityId("CalculatorResults")
    .Text.Replace("Display is", "").Trim();
  Assert.AreEqual(expected, actual);
}

One note, here – to run successfully these tests, you should install the latest NuGet packages for MSTest.TestFramework and MSTest.TestAdapter. I was using the default ones that came with the Visual Studio 2017 templates and this test wasn’t being detected. As soon as I updates the NuGet packages, everything was fine.

Now that we have our first tests in place, let’s continue to test the calculator’s UI. Instead of testing the standard calculator, we will test the programmer’s calculator. In the project, create a new class and name it ProgrammersCalculatorTests.

Add this code for the initialization of the class and tests:

private static WindowsDriver _calculatorSession;

[ClassInitialize]
public static void StartCalculator(TestContext context)
{
  DesiredCapabilities appCapabilities = new DesiredCapabilities();
  appCapabilities.SetCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
  appCapabilities.SetCapability("deviceName", "WindowsPC");
  _calculatorSession = new WindowsDriver(new Uri("http://127.0.0.1:4723"), appCapabilities);
  _calculatorSession.FindElementByAccessibilityId("NavButton").Click();
  _calculatorSession.FindElementByName("Programmer Calculator").Click();
  _calculatorSession.FindElementByAccessibilityId("decimalButton").Click();
}

[ClassCleanup]
public static void CloseCalculator()
{
  _calculatorSession.Dispose();
  _calculatorSession = null;
}

[TestInitialize]
public void ResetCalculator()
{
  _calculatorSession.FindElementByAccessibilityId("decimalButton").Click();
  _calculatorSession.FindElementByName("Clear").Click();
}

The initialization code is similar to the other test, but instead of setting the type of calculator for every test, we will set the programmer’s calculator at the start of the tests. Then, we’ll clear the calculator and reset to the Decimal entry before each test.

The first test we’ll create is to make sure that the display changes to the correct value when you click the Hex, Octal and Binary buttons:

[DataTestMethod]
[DataRow("hexButton","C")]
[DataRow("octolButton","14")]
[DataRow("binaryButton","1100")]
public void Number12ShouldConvertOkToHexOctalAndBinary(string buttonId, string result)
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Two").Click();
  _calculatorSession.FindElementByAccessibilityId(buttonId).Click();
  var actual = _calculatorSession.FindElementByAccessibilityId("CalculatorResults")
    .Text.Replace("Display is", "").Trim();
  Assert.AreEqual(result, actual);
}

We are using the parametrized tests again, passing the ids of the buttons and the expected results for the decimal “12”.

Now, let’s create the tests for the keys that should be enabled/disabled for each mode set:

[TestMethod]
public void InHexModeAllButtonsShouldBeEnabled()
{
  var enabledButtons = new[] {"One","Two","Three" ,"Four","Five","Six",
    "Seven","Eight","Nine","Zero","A", "B", "C", "D", "E", "F" };
  _calculatorSession.FindElementByAccessibilityId("hexButton").Click();
  foreach (var buttonName in enabledButtons)
  {
    Assert.IsTrue(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
  }
}

[TestMethod]
public void InOctalModeButtonsZeroToSevenShouldBeEnabled()
{
  var enabledButtons = new[] { "One", "Two", "Three", "Four", "Five", "Six",
    "Seven", "Zero" };
  _calculatorSession.FindElementByAccessibilityId("octolButton").Click();
  foreach (var buttonName in enabledButtons)
  {
    Assert.IsTrue(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
  }
}  

[TestMethod]
public void InBinaryModeAllButtonsExceptZeroAndOneToSevenShouldBeDisabled()
{
  var disabledButtons = new[] {"Two", "Three", "Four", "Five", "Six",
    "Seven", "Eight", "Nine", "A", "B", "C", "D", "E", "F" };
  _calculatorSession.FindElementByAccessibilityId("binaryButton").Click();
  foreach (var buttonName in disabledButtons)
  {
    Assert.IsFalse(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
  }
}

These tests have several asserts in a single test (the assert is in a foreach loop). Although I don’t like many asserts in a single test, in this case, it makes sense: if only one of these keys is not enabled or disabled, the test should fails. It doesn’t make sense to create one test for every single key. In these tests, I have added a message, so if one fails, I can check which key made the test fail.

The next test are for the operations:

[TestMethod]
public void NotZeroShouldBeMinus1()
{
  _calculatorSession.FindElementByName("Zero").Click();
  _calculatorSession.FindElementByName("Not").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("-1", actual);
}

[TestMethod]
public void NotOneShouldBeMinus2()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Not").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("-2", actual);
}

[TestMethod]
public void OneAndZeroShouldBeZero()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("And").Click();
  _calculatorSession.FindElementByName("Zero").Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("0", actual);
}

[TestMethod]
public void OneOrZeroShouldBeOne()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Or").Click();
  _calculatorSession.FindElementByName("Zero").Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("1", actual);
}

[TestMethod]
public void OneXorZeroShouldBeOne()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Exclusive or").Click();
  _calculatorSession.FindElementByName("Zero").Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("1", actual);
}

[TestMethod]
public void OneLshOneShouldBeTwo()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Left shift").Click();
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("2", actual);
}

[TestMethod]
public void OneRshOneShouldBeZero()
{
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Right shift").Click();
  _calculatorSession.FindElementByName("One").Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual("0", actual);
}

Looking at these tests, you can see some patterns – there are two kinds of tests – for unary operators (NOT) and for binary operators (AND, OR, XOR, LSH, RSH). We can refactor these tests to create parametrized tests (thank you Microsoft for introducing this Smile)

[DataTestMethod]
[DataRow("Zero","-1")]
[DataRow("One","-2")]
[DataRow("Nine","-10")]
public void NotTestsForPositiveNumbersShouldBeOk(string key, string result)
{
  _calculatorSession.FindElementByName(key).Click();
  _calculatorSession.FindElementByName("Not").Click();
  var actual = GetDisplayText();
  Assert.AreEqual(result, actual,$"Test for key {key}");
}

[DataTestMethod]
[DataRow("One", "0")]
[DataRow("Two", "1")]
[DataRow("Nine", "8")]
public void NotTestsForNegativeNumbersShouldBeOk(string key, string result)
{
  _calculatorSession.FindElementByName(key).Click();
  _calculatorSession.FindElementByName("Positive Negative").Click();
  _calculatorSession.FindElementByName("Not").Click();
  var actual = GetDisplayText();
  Assert.AreEqual(result, actual, $"Test for negative {key}");
}

[DataTestMethod]
[DataRow("One","And","Zero","0")]
[DataRow("One","And","One","1")]
[DataRow("One","And","Three","1")]
[DataRow("One", "Or", "Zero", "1")]
[DataRow("One", "Or", "One", "1")]
[DataRow("One", "Or", "Three", "3")]
[DataRow("One", "Exclusive or", "Zero", "1")]
[DataRow("One", "Exclusive or", "One", "0")]
[DataRow("One", "Exclusive or", "Three", "2")]
[DataRow("One", "Left shift", "Zero", "1")]
[DataRow("One", "Left shift", "One", "2")]
[DataRow("One", "Left shift", "Three", "8")]
[DataRow("One", "Right shift", "Zero", "1")]
[DataRow("One", "Right shift", "One", "0")]
[DataRow("One", "Right shift", "Three", "0")]
public void TestsForOperatorsShouldBeOk(string first, string oper, string second, string result)
{
  _calculatorSession.FindElementByName(first).Click();
  _calculatorSession.FindElementByName(oper).Click();
  _calculatorSession.FindElementByName(second).Click();
  _calculatorSession.FindElementByName("Equals").Click();
  var actual = GetDisplayText();
  Assert.AreEqual(result, actual,$"Test for {first} {oper} {second}");
}

Now he tests are cleaner and we also introduced a lot of new tests for the operators. If we want to add a new test here is just a matter of adding a new data row.

Now, we can go on and test another kind of applications: Win32 applications.

Testing the UI of Win32 applications

We have seen how to test the UI of a UWP application, but that’s not all that we can do with Appium. With it, we can test any kind of desktop application, including Win32 applications.

We will test another calculator, the old Windows calculator, downloaded from http://winaero.com/blog/get-calculator-from-windows-8-and-windows-7-in-windows-10/. After downloading and installing it, you can run it by typing calc.exe on the search bar:

image

Now we can create our tests for the calculator. Create a new class and name it OldCalculatorTests. Add this code in the class:

private static WindowsDriver _calculatorSession;

[TestMethod]
public void CalculatorIsNotNull()
{
  Assert.IsNotNull(_calculatorSession);
}

[DataTestMethod]
[DataRow("1", "1")]
[DataRow("2", "2")]
[DataRow("3", "3")]
[DataRow("4", "4")]
[DataRow("5", "5")]
[DataRow("6", "6")]
[DataRow("7", "7")]
[DataRow("8", "8")]
[DataRow("9", "9")]
[DataRow("0", "0")]
public void CalculatorKeysShouldDisplay(string key, string expected)
{
  _calculatorSession.FindElementByName(key).Click();
  var actual = _calculatorSession.FindElementByName("Result").Text.Trim();
  Assert.AreEqual(expected, actual);
}

[ClassInitialize]
public static void StartCalculator(TestContext context)
{
  DesiredCapabilities appCapabilities = new DesiredCapabilities();
  appCapabilities.SetCapability("app", "calc.exe");
  appCapabilities.SetCapability("deviceName", "WindowsPC");
  _calculatorSession = new WindowsDriver(new Uri("http://127.0.0.1:4723"), appCapabilities);
  _calculatorSession.FindElementByName("Calculator").SendKeys(Keys.Alt+"1");
  _calculatorSession.FindElementByName("Clear").Click();
}

[ClassCleanup]
public static void CloseCalculator()
{
  _calculatorSession.Dispose();
  _calculatorSession = null;
}

[TestInitialize]
public void ResetCalculator()
{
  _calculatorSession.FindElementByName("Clear").Click();
}

As you can see, the tests are pretty much the same ones as the UWP calculator. You just have to change the app name to calc.exe (you must add the command line text for activating the program – in this case, the calculator is in the path) and change the control names. Then, the tests are the same. The tests for the Programmer’s calculator are in the ProgrammersOldCalculatorTests:

public class ProgrammersOldCalculatorTests
{
    private static WindowsDriver _calculatorSession;

    [ClassInitialize]
    public static void StartCalculator(TestContext context)
    {
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability("app", "calc.exe");
        appCapabilities.SetCapability("deviceName", "WindowsPC");
        _calculatorSession = new WindowsDriver(new Uri("http://127.0.0.1:4723"), appCapabilities);
        _calculatorSession.FindElementByName("Calculator").SendKeys(Keys.Alt + "3");
        _calculatorSession.FindElementByName("Clear").Click();
    }

    [ClassCleanup]
    public static void CloseCalculator()
    {
        _calculatorSession.Dispose();
        _calculatorSession = null;
    }

    [TestInitialize]
    public void ResetCalculator()
    {
        _calculatorSession.FindElementByName("Decimal").Click();
        _calculatorSession.FindElementByName("Clear").Click();
    }

    [DataTestMethod]
    [DataRow("Hexadecimal", "C")]
    [DataRow("Octal", "14")]
    [DataRow("Binary", "1100")]
    public void Number12ShouldConvertOkToHexOctalAndBinary(string buttonId, string result)
    {
        _calculatorSession.FindElementByName("1").Click();
        _calculatorSession.FindElementByName("2").Click();
        _calculatorSession.FindElementByName(buttonId).Click();
        var actual = GetDisplayText();
        Assert.AreEqual(result, actual);
    }

    private static string GetDisplayText()
    {
        return _calculatorSession.FindElementByAccessibilityId("Result").Text.Trim();
    }

    [TestMethod]
    public void InDecimalModeLetterButtonsShouldBeDisabled()
    {
        var disabledButtons = new[] { "A", "B", "C", "D", "E", "F" };
        foreach (var buttonName in disabledButtons)
        {
            Assert.IsFalse(_calculatorSession.FindElementByName(buttonName).Enabled);
        }
    }

    [TestMethod]
    public void InHexModeAllButtonsShouldBeEnabled()
    {
        var enabledButtons = new[] {"1","2","3" ,"4","5","6",
            "7","8","9","0","A", "B", "C", "D", "E", "F" };
        _calculatorSession.FindElementByAccessibilityId("hexButton").Click();
        foreach (var buttonName in enabledButtons)
        {
            Assert.IsTrue(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
        }
    }

    [TestMethod]
    public void InOctalModeButtonsZeroToSevenShouldBeEnabled()
    {
        var enabledButtons = new[] { "1", "2", "3", "4", "5", "6",
            "7", "0" };
        _calculatorSession.FindElementByAccessibilityId("octolButton").Click();
        foreach (var buttonName in enabledButtons)
        {
            Assert.IsTrue(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
        }
    }

    [TestMethod]
    public void InBinaryModeAllButtonsExceptZeroAndOneToSevenShouldBeDisabled()
    {
        var disabledButtons = new[] {"2", "3", "4", "5", "6",
           "7", "8", "9", "A", "B", "C", "D", "E", "F" };
        _calculatorSession.FindElementByAccessibilityId("binaryButton").Click();
        foreach (var buttonName in disabledButtons)
        {
            Assert.IsFalse(_calculatorSession.FindElementByName(buttonName).Enabled, $"Test for {buttonName}");
        }
    }

    [DataTestMethod]
    [DataRow("0", "-1")]
    [DataRow("1", "-2")]
    [DataRow("9", "-10")]
    public void NotTestsForPositiveNumbersShouldBeOk(string key, string result)
    {
        _calculatorSession.FindElementByName(key).Click();
        _calculatorSession.FindElementByName("Not").Click();
        var actual = GetDisplayText();
        Assert.AreEqual(result, actual, $"Test for key {key}");
    }

    [DataTestMethod]
    [DataRow("1", "0")]
    [DataRow("2", "1")]
    [DataRow("9", "8")]
    public void NotTestsForNegativeNumbersShouldBeOk(string key, string result)
    {
        _calculatorSession.FindElementByName(key).Click();
        _calculatorSession.FindElementByName("Negate").Click();
        _calculatorSession.FindElementByName("Not").Click();
        var actual = GetDisplayText();
        Assert.AreEqual(result, actual, $"Test for negative {key}");
    }

    [DataTestMethod]
    [DataRow("1", "And", "0", "0")]
    [DataRow("1", "And", "1", "1")]
    [DataRow("1", "And", "3", "1")]
    [DataRow("1", "Or", "0", "1")]
    [DataRow("1", "Or", "1", "1")]
    [DataRow("1", "Or", "3", "3")]
    [DataRow("1", "Exclusive or", "0", "1")]
    [DataRow("1", "Exclusive or", "1", "0")]
    [DataRow("1", "Exclusive or", "3", "2")]
    [DataRow("1", "Left shift", "0", "1")]
    [DataRow("1", "Left shift", "1", "2")]
    [DataRow("1", "Left shift", "3", "8")]
    [DataRow("1", "Right shift", "0", "1")]
    [DataRow("1", "Right shift", "1", "0")]
    [DataRow("1", "Right shift", "3", "0")]
    public void TestsForOperatorsShouldBeOk(string first, string oper, string second, string result)
    {
        _calculatorSession.FindElementByName(first).Click();
        _calculatorSession.FindElementByName(oper).Click();
        _calculatorSession.FindElementByName(second).Click();
        _calculatorSession.FindElementByName("Equals").Click();
        var actual = GetDisplayText();
        Assert.AreEqual(result, actual, $"Test for {first} {oper} {second}");
    }

}

As you can see, there is not much difference from the previous tests. Testing a Win32 application is almost the same as testing an UWP app.

Conclusions

With Appium, you can test the UI of your application the same way you do with you unit tests: you can use the same framework and create UI tests similar to unit tests. This is a great bonus, as there is almost no learning curve and, as an added bonus, you can test all your desktop applications the same way, whether they are UWP, .NET or Win32 apps. Nice, no?

All the source code for this article is in https://github.com/bsonnino/CalcTests