Writing unit tests with Assert
This tutorial shows how to write a small function, add tests for it using the Assert package, and run those tests from inside the IDE.
- The Assert package
- Adding the package
- The function under test
- Writing the tests
- Running the tests
- Testing error paths
- Choosing the right module
- Test organisation
- Where to go next
The Assert package
The Assert package provides three modules — Exact, Strict, and Permissive — that share the same fifteen-member API:
| Module | String comparison | Numeric datatype must match |
|---|---|---|
| Exact | Case-sensitive | Yes — 5 and 5.0 are not equal |
| Strict | Case-sensitive | No |
| Permissive | Case-insensitive | No |
All three compile out of release builds: every member is tagged [DebugOnly(True)], so assertion calls have zero runtime cost in production EXEs. Tests live in the same project as production code and run in the IDE under the full debugger.
The most commonly used members are:
Exact.AreEqual expected, actual– fails if the two values differExact.IsTrue condition– fails if the condition isFalseExact.IsFalse condition– fails if the condition isTrueExact.Fail message– unconditionally records a failureExact.Succeed– explicitly records a pass (useful at the end of conditional paths)
Each failing assertion records the source location, the expected and actual values, and the optional message string. Results appear in the Debug Console pane.
Adding the package
Open Project → References (Ctrl+T) → Available Packages and tick Assert. Click OK. The three modules (Exact, Strict, Permissive) are now in scope without any Imports statement.
The function under test
Add a standard Module to the project (right-click the project in the Project Explorer, then Add → Module). Name it StringUtils. Add the following function:
' Pads s on the left with padChar until it reaches totalWidth characters.
' If s is already at or beyond totalWidth, it is returned unchanged.
Public Function PadLeft(ByVal s As String, _
ByVal totalWidth As Long, _
Optional ByVal padChar As String = " ") As String
If Len(s) >= totalWidth Then
PadLeft = s
Else
PadLeft = String(totalWidth - Len(s), Left$(padChar, 1)) & s
End If
End Function
PadLeft is a good test subject: it has a clear specification, an optional parameter with a default, and several distinct edge cases.
Writing the tests
Add a second module, TestStringUtils. Each test is a Public Sub that exercises one aspect of the function. Keep each Sub short — ideally one logical scenario per Sub, named to describe what it checks.
Public Sub TestPadLeft_Normal()
' Three spaces prefix "hi" to reach width 5
Exact.AreEqual " hi", PadLeft("hi", 5)
End Sub
Public Sub TestPadLeft_CustomPadChar()
' Zero-pad to width 5
Exact.AreEqual "00042", PadLeft("42", 5, "0")
End Sub
Public Sub TestPadLeft_AtWidth()
' Already at width -- no change
Exact.AreEqual "hello", PadLeft("hello", 5)
End Sub
Public Sub TestPadLeft_ExceedsWidth()
' Already longer than width -- not truncated
Exact.AreEqual "toolong", PadLeft("toolong", 5)
End Sub
Public Sub TestPadLeft_EmptyString()
' Empty input -- result is all padding
Exact.AreEqual " ", PadLeft("", 3)
End Sub
Public Sub TestPadLeft_SingleChar()
' Width of 1, input already 1 char -- no change
Exact.AreEqual "x", PadLeft("x", 1)
End Sub
These tests cover: the normal case, a custom pad character, the at-boundary case, the over-boundary case, an empty input, and a minimal input.
Running the tests
There are two ways to run a test Sub:
-
CodeLens — place the cursor anywhere inside a test Sub. The CodeLens bar above the
Subline shows a▶ Runbutton. Click it to run that one Sub. The result appears immediately in the Debug Console. -
F5 from inside the Sub — place the cursor inside the Sub and press F5. twinBASIC runs the procedure and stops when it returns or when an assertion fails.
To run all tests in a batch, add a runner Sub that calls each test in sequence:
Public Sub RunAllTests()
TestPadLeft_Normal
TestPadLeft_CustomPadChar
TestPadLeft_AtWidth
TestPadLeft_ExceedsWidth
TestPadLeft_EmptyString
TestPadLeft_SingleChar
Debug.Print "All PadLeft tests passed."
End Sub
Place the cursor inside RunAllTests and press F5 (or click ▶ Run in the CodeLens bar). If any assertion fails, execution stops at the failing line and the Debug Console shows which assertion failed, its expected and actual values, and the source location.
Testing error paths
Sometimes a function should raise an error for bad input. Test that with On Error Resume Next and Err.Number:
Public Sub TestPadLeft_ZeroWidth()
' A width of 0 is technically valid -- the string is returned unchanged
' if it is already zero-length, and unchanged otherwise.
Exact.AreEqual "hi", PadLeft("hi", 0)
Exact.AreEqual "", PadLeft("", 0)
End Sub
If instead you expected the function to raise an error:
Public Sub TestSomethingThatShouldRaise()
On Error Resume Next
SomeFunctionThatRaises 0 ' call that should fail
If Err.Number = 0 Then
Exact.Fail "expected an error, but none was raised"
End If
On Error GoTo 0
End Sub
Choosing the right module
Use Exact by default — its strictest comparison semantics prevent tests from passing for the wrong reason. Switch to Strict or Permissive when the code under test is intentionally case-insensitive or when you are comparing values that should be equal regardless of numeric type:
' Exact would fail because "hello" ≠ "Hello" (case differs)
Strict.AreEqual "HELLO", LCase$("HELLO") ' fails -- "hello" ≠ "HELLO"
Permissive.AreEqual "HELLO", LCase$("HELLO") ' passes -- case-insensitive
The three modules are documented in full at:
- Exact module – strictest semantics
- Strict module – case-sensitive strings, type-lenient numeric
- Permissive module – case-insensitive strings, type-lenient numeric
Test organisation
As a project grows, keep tests close to the code they exercise. One common convention:
- One production module per concern:
StringUtils,DateUtils,FileHelpers, … - One test module per production module:
TestStringUtils,TestDateUtils,TestFileHelpers, … - A top-level
RunAllSub in aTestRunnermodule that calls each module’s runner
Because all test Subs are compiled out of release builds ([DebugOnly(True)]), this organisation adds no overhead to the shipped executable.
Where to go next
- Assert package reference – all fifteen members in detail: Assert package
- Forms basics – building a form to host a small test harness visually: Forms basics
- Windows API – writing and testing a function that wraps a Declare: Calling the Windows API