Skip to main content

WPF C#/VB -
Unit & End-to-End Testing

UnitTests.cs

// AZUL CODING ---------------------------------------
// WPF C#/VB - Unit & End-to-End Testing
// https://youtu.be/pZQBH8gg8iE


using AzulCalculator;

namespace TestSuite
{
    public class UnitTests
    {
        [Theory]
        [InlineData(2, 3, 5)]
        [InlineData(-1, 1, 0)]
        [InlineData(0, 0, 0)]
        [InlineData(10.5, 2.3, 12.8)]
        [InlineData(-5.5, -3.2, -8.7)]
        public void Add_ShouldReturnCorrectSum(double a, double b, double expected)
        {
            double result = Calculator.Add(a, b);
            Assert.Equal(expected, result, 1);
        }

        [Fact]
        public void Add_WithLargeNumbers_ShouldNotOverflow()
        {
            double a = double.MaxValue / 2;
            double b = double.MaxValue / 4;

            double result = Calculator.Add(a, b);

            Assert.True(result > 0);
            Assert.False(double.IsInfinity(result));
        }

        [Theory]
        [InlineData(10, 0)]
        [InlineData(-5, 0)]
        [InlineData(0, 0)]
        [InlineData(double.MaxValue, 0)]
        public void Divide_ByZero_ShouldThrowArgumentException(double a, double b)
        {
            ArgumentException exception = Assert.Throws<ArgumentException>(() =>
                Calculator.Divide(a, b));

            Assert.Equal("Cannot divide by zero", exception.Message);
        }
    }
}

Help support the channel

UITests.cs

// AZUL CODING ---------------------------------------
// WPF C#/VB - Unit & End-to-End Testing
// https://youtu.be/pZQBH8gg8iE


using System.Diagnostics;
using System.Windows.Automation;

namespace TestSuite
{
    public class UITests : IDisposable
    {
        private Process? _appProcess;
        private AutomationElement? _mainWindow;
        private readonly string _appPath;

        // change these values as appropriate
        private const string _appName = "AzulCalculator";
        private const string _appWindowTitle = "Azul Coding - Simple Calculator";

        public UITests()
        {
            _appPath = Path.Combine(
                Directory.GetCurrentDirectory(),
                "..", "..", "..", "..", _appName, "bin", "Debug", $"net{Environment.Version.Major}.0-windows", $"{_appName}.exe"
            );
        }

        #region Helper Functions

        private async Task<AutomationElement> StartApplicationAndGetMainWindow()
        {
            if (!File.Exists(_appPath))
                throw new FileNotFoundException($"Application not found at: {_appPath}");

            _appProcess = Process.Start(_appPath);
            await Task.Delay(2000);

            int retryCount = 0;
            int maxRetries = 10;

            while (retryCount < maxRetries)
            {
                AutomationElement desktop = AutomationElement.RootElement;
                PropertyCondition condition = new(AutomationElement.NameProperty, _appWindowTitle);
                _mainWindow = desktop.FindFirst(TreeScope.Children, condition);

                if (_mainWindow != null)
                    return _mainWindow;

                await Task.Delay(500);
                retryCount++;
            }

            throw new TimeoutException("Could not find the main window within the timeout period");
        }

        private AutomationElement FindElementByAutomationId(string automationId)
        {
            if (_mainWindow == null)
                throw new InvalidOperationException("Window is not initialised");

            PropertyCondition condition = new(AutomationElement.AutomationIdProperty, automationId);
            AutomationElement element = _mainWindow.FindFirst(TreeScope.Descendants, condition);

            return element ?? throw new InvalidOperationException($"Could not find element with AutomationId: {automationId}");
        }

        private static void ClickButton(AutomationElement button)
        {
            if (button.TryGetCurrentPattern(InvokePattern.Pattern, out var pattern))
            {
                ((InvokePattern)pattern).Invoke();
            }
            else
            {
                throw new InvalidOperationException("Button does not support InvokePattern");
            }
        }

        private static void SetTextBoxValue(AutomationElement textBox, string value)
        {
            if (textBox.TryGetCurrentPattern(ValuePattern.Pattern, out var pattern))
            {
                ((ValuePattern)pattern).SetValue(value);
            }
            else
            {
                throw new InvalidOperationException("TextBox does not support ValuePattern");
            }
        }

        private static string GetTextValue(AutomationElement element)
        {
            return element.Current.Name ?? string.Empty;
        }

        public void Dispose()
        {
            try
            {
                _appProcess?.CloseMainWindow();
                _appProcess?.WaitForExit(3000);

                if (!_appProcess?.HasExited == true)
                    _appProcess?.Kill();
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Error disposing application: {ex.Message}");
            }
            finally
            {
                _appProcess?.Dispose();
                GC.SuppressFinalize(this);
            }
        }

        #endregion
        #region Tests

        [Fact]
        public async Task Addition_ShouldDisplayCorrectResult()
        {
            await StartApplicationAndGetMainWindow();
            AutomationElement firstNumberTextBox = FindElementByAutomationId("FirstNumber");
            AutomationElement secondNumberTextBox = FindElementByAutomationId("SecondNumber");
            AutomationElement addButton = FindElementByAutomationId("AddButton");
            AutomationElement resultTextBlock = FindElementByAutomationId("Result");

            SetTextBoxValue(firstNumberTextBox, "10");
            SetTextBoxValue(secondNumberTextBox, "5");
            ClickButton(addButton);

            await Task.Delay(500);

            string result = GetTextValue(resultTextBlock);
            Assert.Equal("15.00", result);
        }

        [Fact]
        public async Task DivisionByZero_ShouldDisplayErrorMessage()
        {
            await StartApplicationAndGetMainWindow();
            AutomationElement firstNumberTextBox = FindElementByAutomationId("FirstNumber");
            AutomationElement secondNumberTextBox = FindElementByAutomationId("SecondNumber");
            AutomationElement divideButton = FindElementByAutomationId("DivideButton");
            AutomationElement errorTextBlock = FindElementByAutomationId("ErrorMessage");

            SetTextBoxValue(firstNumberTextBox, "10");
            SetTextBoxValue(secondNumberTextBox, "0");
            ClickButton(divideButton);

            await Task.Delay(500);

            string errorMessage = GetTextValue(errorTextBlock);
            Assert.Equal("Cannot divide by zero", errorMessage);
        }

        #endregion
    }
}

Calculator.cs

// AZUL CODING ---------------------------------------
// WPF C#/VB - Unit & End-to-End Testing
// https://youtu.be/pZQBH8gg8iE


namespace AzulCalculator
{
    public class Calculator
    {
        public static double Add(double a, double b)
        {
            return a + b;
        }

        public static double Subtract(double a, double b)
        {
            return a - b;
        }

        public static double Multiply(double a, double b)
        {
            return a * b;
        }

        public static double Divide(double a, double b)
        {
            if (b == 0)
                throw new ArgumentException("Cannot divide by zero");
            return a / b;
        }
    }
}

MainWindow.xaml.cs

// AZUL CODING ---------------------------------------
// WPF C#/VB - Unit & End-to-End Testing
// https://youtu.be/pZQBH8gg8iE


using System.Windows;

namespace AzulCalculator
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void AddButton_Click(object sender, RoutedEventArgs e)
        {
            PerformOperation(Calculator.Add);
        }

        private void SubtractButton_Click(object sender, RoutedEventArgs e)
        {
            PerformOperation(Calculator.Subtract);
        }

        private void MultiplyButton_Click(object sender, RoutedEventArgs e)
        {
            PerformOperation(Calculator.Multiply);
        }

        private void DivideButton_Click(object sender, RoutedEventArgs e)
        {
            PerformOperation(Calculator.Divide);
        }

        private void ClearButton_Click(object sender, RoutedEventArgs e)
        {
            FirstNumberTextBox.Text = "";
            SecondNumberTextBox.Text = "";
            ResultTextBlock.Text = "";
            ErrorTextBlock.Text = "";
        }

        private void PerformOperation(Func<double, double, double> operation)
        {
            try
            {
                ErrorTextBlock.Text = "";

                if (!double.TryParse(FirstNumberTextBox.Text, out double first))
                {
                    ErrorTextBlock.Text = "Please enter a valid first number.";
                    return;
                }

                if (!double.TryParse(SecondNumberTextBox.Text, out double second))
                {
                    ErrorTextBlock.Text = "Please enter a valid second number.";
                    return;
                }

                double result = operation(first, second);
                ResultTextBlock.Text = result.ToString("F2");
            }
            catch (ArgumentException ex)
            {
                ErrorTextBlock.Text = ex.Message;
                ResultTextBlock.Text = "";
            }
            catch (Exception ex)
            {
                ErrorTextBlock.Text = $"An error occurred: {ex.Message}";
                ResultTextBlock.Text = "";
            }
        }
    }
}

UnitTests.vb

' AZUL CODING ---------------------------------------
' WPF C#/VB - Unit & End-to-End Testing
' https://youtu.be/pZQBH8gg8iE


Imports AzulCalculator
Imports Xunit

Public Class UnitTests

    <Theory>
    <InlineData(2, 3, 5)>
    <InlineData(-1, 1, 0)>
    <InlineData(0, 0, 0)>
    <InlineData(10.5, 2.3, 12.8)>
    <InlineData(-5.5, -3.2, -8.7)>
    Public Sub Add_ShouldReturnCorrectSum(a As Double, b As Double, expected As Double)
        Dim result As Double = Calculator.Add(a, b)
        Assert.Equal(expected, result, 1)
    End Sub

    <Fact>
    Public Sub Add_WithLargeNumbers_ShouldNotOverflow()
        Dim a As Double = Double.MaxValue / 2
        Dim b As Double = Double.MaxValue / 4

        Dim result As Double = Calculator.Add(a, b)

        Assert.True(result > 0)
        Assert.False(Double.IsInfinity(result))
    End Sub

    <Theory>
    <InlineData(10, 0)>
    <InlineData(-5, 0)>
    <InlineData(0, 0)>
    <InlineData(Double.MaxValue, 0)>
    Public Sub Divide_ByZero_ShouldThrowArgumentException(a As Double, b As Double)
        Dim exception As ArgumentException = Assert.Throws(Of ArgumentException)(
                Function() Calculator.Divide(a, b))

        Assert.Equal("Cannot divide by zero", exception.Message)
    End Sub

End Class

UITests.vb

' AZUL CODING ---------------------------------------
' WPF C#/VB - Unit & End-to-End Testing
' https://youtu.be/pZQBH8gg8iE


Imports System.IO
Imports System.Windows.Automation
Imports Xunit

Public Class UITests
    Implements IDisposable

    Private _appProcess As Process
    Private _mainWindow As AutomationElement
    Private ReadOnly _appPath As String

    ' change these values as appropriate
    Private Const _appName As String = "AzulCalculator"
    Private Const _appWindowTitle As String = "Azul Coding - Simple Calculator"

    Public Sub New()
        _appPath = Path.Combine(
            Directory.GetCurrentDirectory(),
            "..", "..", "..", "..", _appName, "bin", "Debug", $"net{Environment.Version.Major}.0-windows", $"{_appName}.exe"
        )
    End Sub

#Region "Helper Functions"

    Private Async Function StartApplicationAndGetMainWindow() As Task(Of AutomationElement)
        If Not File.Exists(_appPath) Then
            Throw New FileNotFoundException($"Application not found at: {_appPath}")
        End If

        _appProcess = Process.Start(_appPath)
        Await Task.Delay(2000)

        Dim retryCount As Integer = 0
        Dim maxRetries As Integer = 10

        While retryCount < maxRetries
            Dim desktop As AutomationElement = AutomationElement.RootElement
            Dim condition As New PropertyCondition(AutomationElement.NameProperty, _appWindowTitle)
            _mainWindow = desktop.FindFirst(TreeScope.Children, condition)

            If _mainWindow IsNot Nothing Then
                Return _mainWindow
            End If

            Await Task.Delay(500)
            retryCount += 1
        End While

        Throw New TimeoutException("Could not find the main window within the timeout period")
    End Function

    Private Function FindElementByAutomationId(automationId As String) As AutomationElement
        If _mainWindow Is Nothing Then
            Throw New InvalidOperationException("Window is not initialised")
        End If

        Dim condition As New PropertyCondition(AutomationElement.AutomationIdProperty, automationId)
        Dim element As AutomationElement = _mainWindow.FindFirst(TreeScope.Descendants, condition)

        If element Is Nothing Then
            Throw New InvalidOperationException($"Could not find element with AutomationId: {automationId}")
        End If

        Return element
    End Function

    Private Shared Sub ClickButton(button As AutomationElement)
        Dim pattern As Object = Nothing
        If button.TryGetCurrentPattern(InvokePattern.Pattern, pattern) Then
            DirectCast(pattern, InvokePattern).Invoke()
        Else
            Throw New InvalidOperationException("Button does not support InvokePattern")
        End If
    End Sub

    Private Shared Sub SetTextBoxValue(textBox As AutomationElement, value As String)
        Dim pattern As Object = Nothing
        If textBox.TryGetCurrentPattern(ValuePattern.Pattern, pattern) Then
            DirectCast(pattern, ValuePattern).SetValue(value)
        Else
            Throw New InvalidOperationException("TextBox does not support ValuePattern")
        End If
    End Sub

    Private Shared Function GetTextValue(element As AutomationElement) As String
        Return If(element.Current.Name, String.Empty)
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        Try
            _appProcess?.CloseMainWindow()
            _appProcess?.WaitForExit(3000)

            If _appProcess?.HasExited = False Then
                _appProcess?.Kill()
            End If
        Catch ex As Exception
            Debug.WriteLine($"Error disposing application: {ex.Message}")
        Finally
            _appProcess?.Dispose()
            GC.SuppressFinalize(Me)
        End Try
    End Sub

#End Region
#Region "Tests"

    <Fact>
    Public Async Function Addition_ShouldDisplayCorrectResult() As Task
        Await StartApplicationAndGetMainWindow()
        Dim firstNumberTextBox As AutomationElement = FindElementByAutomationId("FirstNumber")
        Dim secondNumberTextBox As AutomationElement = FindElementByAutomationId("SecondNumber")
        Dim addButton As AutomationElement = FindElementByAutomationId("AddButton")
        Dim resultTextBlock As AutomationElement = FindElementByAutomationId("Result")

        SetTextBoxValue(firstNumberTextBox, "10")
        SetTextBoxValue(secondNumberTextBox, "5")
        ClickButton(addButton)

        Await Task.Delay(500)

        Dim result As String = GetTextValue(resultTextBlock)
        Assert.Equal("15.00", result)
    End Function

    <Fact>
    Public Async Function DivisionByZero_ShouldDisplayErrorMessage() As Task
        Await StartApplicationAndGetMainWindow()
        Dim firstNumberTextBox As AutomationElement = FindElementByAutomationId("FirstNumber")
        Dim secondNumberTextBox As AutomationElement = FindElementByAutomationId("SecondNumber")
        Dim divideButton As AutomationElement = FindElementByAutomationId("DivideButton")
        Dim errorTextBlock As AutomationElement = FindElementByAutomationId("ErrorMessage")

        SetTextBoxValue(firstNumberTextBox, "10")
        SetTextBoxValue(secondNumberTextBox, "0")
        ClickButton(divideButton)

        Await Task.Delay(500)

        Dim errorMessage As String = GetTextValue(errorTextBlock)
        Assert.Equal("Cannot divide by zero", errorMessage)
    End Function

#End Region
End Class

Calculator.vb

' AZUL CODING ---------------------------------------
' WPF C#/VB - Unit & End-to-End Testing
' https://youtu.be/pZQBH8gg8iE


Public Class Calculator

    Public Shared Function Add(a As Double, b As Double) As Double
        Return a + b
    End Function

    Public Shared Function Subtract(a As Double, b As Double) As Double
        Return a - b
    End Function

    Public Shared Function Multiply(a As Double, b As Double) As Double
        Return a * b
    End Function

    Public Shared Function Divide(a As Double, b As Double) As Double
        If b = 0 Then
            Throw New ArgumentException("Cannot divide by zero")
        End If
        Return a / b
    End Function

End Class

MainWindow.xaml.vb

' AZUL CODING ---------------------------------------
' WPF C#/VB - Unit & End-to-End Testing
' https://youtu.be/pZQBH8gg8iE


Class MainWindow

    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub AddButton_Click(sender As Object, e As RoutedEventArgs)
        PerformOperation(AddressOf Calculator.Add)
    End Sub

    Private Sub SubtractButton_Click(sender As Object, e As RoutedEventArgs)
        PerformOperation(AddressOf Calculator.Subtract)
    End Sub

    Private Sub MultiplyButton_Click(sender As Object, e As RoutedEventArgs)
        PerformOperation(AddressOf Calculator.Multiply)
    End Sub

    Private Sub DivideButton_Click(sender As Object, e As RoutedEventArgs)
        PerformOperation(AddressOf Calculator.Divide)
    End Sub

    Private Sub ClearButton_Click(sender As Object, e As RoutedEventArgs)
        FirstNumberTextBox.Text = ""
        SecondNumberTextBox.Text = ""
        ResultTextBlock.Text = ""
        ErrorTextBlock.Text = ""
    End Sub

    Private Sub PerformOperation(operation As Func(Of Double, Double, Double))
        Try
            ErrorTextBlock.Text = ""

            Dim first As Double
            If Not Double.TryParse(FirstNumberTextBox.Text, first) Then
                ErrorTextBlock.Text = "Please enter a valid first number."
                Return
            End If

            Dim second As Double
            If Not Double.TryParse(SecondNumberTextBox.Text, second) Then
                ErrorTextBlock.Text = "Please enter a valid second number."
                Return
            End If

            Dim result As Double = operation(first, second)
            ResultTextBlock.Text = result.ToString("F2")
        Catch ex As ArgumentException
            ErrorTextBlock.Text = ex.Message
            ResultTextBlock.Text = ""
        Catch ex As Exception
            ErrorTextBlock.Text = $"An error occurred: {ex.Message}"
            ResultTextBlock.Text = ""
        End Try
    End Sub

End Class

XAML

<!-- AZUL CODING --------------------------------------- -->
<!-- WPF C#/VB - Unit & End-to-End Testing -->
<!-- https://youtu.be/pZQBH8gg8iE -->


<Window x:Class="AzulCalculator.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="Azul Coding - Simple Calculator" Background="#03506E" SizeToContent="WidthAndHeight" Foreground="White" ResizeMode="CanMinimize">
    <StackPanel Margin="20">
        <TextBlock Text="Simple calculator" FontSize="20" FontWeight="DemiBold" Margin="0,0,0,20"/>

        <DockPanel Margin="0,5">
            <TextBlock Text="First number:" MinWidth="115" VerticalAlignment="Center"/>
            <TextBox x:Name="FirstNumberTextBox" Padding="5" AutomationProperties.AutomationId="FirstNumber"/>
        </DockPanel>
        <DockPanel Margin="0,5">
            <TextBlock Text="Second number:" MinWidth="115" VerticalAlignment="Center"/>
            <TextBox x:Name="SecondNumberTextBox" Padding="5" AutomationProperties.AutomationId="SecondNumber"/>
        </DockPanel>

        <StackPanel Orientation="Horizontal" Margin="0,20,0,10">
            <Button x:Name="AddButton" Content="+" Width="40" Height="30" Margin="0,0,10,0" FontSize="20" Click="AddButton_Click" AutomationProperties.AutomationId="AddButton"/>
            <Button x:Name="SubtractButton" Content="−" Width="40" Height="30" Margin="0,0,10,0" FontSize="20" Click="SubtractButton_Click" AutomationProperties.AutomationId="SubtractButton"/>
            <Button x:Name="MultiplyButton" Content="×" Width="40" Height="30" Margin="0,0,10,0" FontSize="20" Click="MultiplyButton_Click" AutomationProperties.AutomationId="MultiplyButton"/>
            <Button x:Name="DivideButton" Content="÷" Width="40" Height="30" Margin="0,0,10,0" FontSize="20" Click="DivideButton_Click" AutomationProperties.AutomationId="DivideButton"/>
            <Button x:Name="ClearButton" Content="Clear" Width="60" Height="30" Click="ClearButton_Click" AutomationProperties.AutomationId="ClearButton"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal" Margin="0,15,0,10">
            <TextBlock Text="Result:" VerticalAlignment="Center" FontWeight="Bold"/>
            <TextBlock x:Name="ResultTextBlock" Width="150" FontSize="20" Margin="15,0,0,0" FontWeight="Bold" AutomationProperties.AutomationId="Result"/>
        </StackPanel>
        <TextBlock x:Name="ErrorTextBlock" Foreground="#FF9696" TextWrapping="Wrap" VerticalAlignment="Top" AutomationProperties.AutomationId="ErrorMessage"/>
    </StackPanel>
</Window>