PBUnit 2025: Unit Testing for PowerBuilder 2025 Solutions
PBUnit 2025 is a unit testing framework for PowerBuilder 2025 solution-format projects (.pbsln / .pbproj). You write tests in PowerScript, run them with one command, and get results that people, build scripts and CI servers can all read.
Source code: https://github.com/bruce-armstrong/PBUnit2025
Background
PBUnit 2025 is based on PBUnit, an xUnit-style framework for PowerBuilder that is no longer maintained. Its test model still works well: a test case is a class, each test is an event, and setup and teardown run around every test. Its GUI runner, however, only reads the old .pbt target files and loads libraries at run time, so it cannot be used with PowerBuilder 2025 solutions.
PBUnit 2025 keeps the original test model and replaces the runner. It adds:
- More assertions:
assertEqualsfor common types,assertTrue,assertFalse,assertNull,assertRowCountandassertThrows. - Automatic test discovery, so there is no list of test classes to maintain.
- Runtime errors inside a test are reported as that test's error, and the run continues.
- Text and JUnit XML results, and a process exit code that build scripts and CI servers can check.
- A PowerShell script that builds the test project with PBAutoBuild, runs it and prints the results.
Requirements
- PowerBuilder 2025 (the script uses PBAutoBuild 25,
pbautobuild250.exe) - PowerShell 7 (
pwsh) to run the build-and-test script
What is in the repository
| Path | What it is |
|---|---|
pbunit.pbl |
Core: testcase, testsuite, testresult and the assertions |
pbunitfunc.pbl |
f_catchsystemerror, for an application's systemerror event |
pbunitrunner.pbl |
n_pbunit_runner (command line, discovery, reports) and n_pbunit_result |
scripts/Invoke-PBUnit.ps1 |
Builds a test project, runs it and prints the results |
selftest/ and pbunit_selftest.pbproj |
The framework's own tests, and an example test application |
Adding PBUnit 2025 to a project
-
Add the repository to your project, for example as a git submodule:
git submodule add github.com/bruce-armstrong/PBUnit2025.git tests/framework
PowerBuilder's Connect to Workspace/Solution does not download git submodules. Clone your repository with GitHub Desktop or
git clone --recurse-submodules, then open the solution in PowerBuilder with File > Open. Until PowerBuilder supports submodules, it shows the framework libraries as new, uncommitted objects; do not commit them from PowerBuilder. -
Create a test project (
.pbproj) whose entry library holds your test application and test cases. List the PBUnit libraries after it, followed by the libraries under test:<Libraries AppEntry="tests\mytests.pbl">
<Library Path="tests\mytests.pbl"/>
<Library Path="tests\framework\pbunitrunner.pbl"/>
<Library Path="tests\framework\pbunit.pbl"/>
<Library Path="tests\framework\pbunitfunc.pbl"/>
<Library Path="src\myapp.pbl"/>
</Libraries> -
Create the test application. Copy
selftest/selftest.pbl/pbunit_selftest.sraand adjust the globals your code expects (a PFC application, for example, declaressqlcaasn_trand createsgnv_app). Theopenevent only needs:n_pbunit_runner lnv_runner
Integer li_exitcode
lnv_runner = create n_pbunit_runner
li_exitcode = lnv_runner.of_Run(CommandParm())
lnv_runner.of_Exit(li_exitcode)of_Exitends the process at once, so do any cleanup (such as destroyinggnv_app) before calling it. -
Exclude the build output folder and the generated
n_pbunit_registry.sru(in the entry library) from source control.
Writing tests
A test case is a class that inherits from testcase and whose name ends in tests. Each test is an event whose name starts with test and that takes no arguments. Every test runs on a new instance of the class, with the setup and teardown events around it.
For example, tests for a hypothetical pricing object in your application:
global type n_cst_pricing_tests from testcase
event test_discount_over_threshold ( )
event test_no_discount_below_threshold ( )
end type
type variables
Protected:
n_cst_pricing inv_pricing
end variables
event setup;call super::setup;inv_pricing = create n_cst_pricing
end event
event teardown;call super::teardown;destroy inv_pricing
end event
event test_discount_over_threshold();Decimal ldc_expected = 90
assertEquals("Price after discount", ldc_expected, inv_pricing.of_Price(100))
end event
event test_no_discount_below_threshold();Decimal ldc_expected = 50
assertEquals("Price without discount", ldc_expected, inv_pricing.of_Price(50))
end event
A failing assertion records a message such as Price after discount: expected <90> but was <100>, and the test continues to its next line.
Assertions
Each assertion returns true if it passed. Most have an overload that takes a message as the first argument, which is prefixed to the failure text.
| Assertion | Failure text |
|---|---|
assertEquals(expected, actual) for string, long, decimal, boolean, date, datetime |
expected <10> but was <20> |
assertTrue(condition), assertFalse(condition) |
Expected true / Expected false |
assertNull(value), assertNotNull(value) |
Value is not null / Value is null |
assertIsValid(object) |
Object is not valid |
assertRowCount(datawindow or datastore, expected) |
datastore row count: expected <2> but was <0> |
assertThrows("event_name") |
Expected event event_name to throw |
assert(message, condition), fail(message) |
the message |
Two null values are equal; a null and a non-null value are not.
assertThrows triggers an event on the test case and passes if it throws, including PowerBuilder runtime errors. PowerScript has no closures, so put the code that should throw in its own event, and do not start that event's name with test or it will also run as a test.
Running tests
pwsh -File tests\framework\scripts\Invoke-PBUnit.ps1 -Project mytests.pbproj
pwsh -File tests\framework\scripts\Invoke-PBUnit.ps1 -Project mytests.pbproj -Filter "n_cst_pricing_tests.test_discount*"
The script:
- finds the test classes in the project's libraries and writes
n_pbunit_registry.sruinto the entry library, - builds the project with
pbautobuild250.exe /pbc /dand moves the PBDs next to the exe, - runs the exe and prints
results.txt;results.xml(JUnit format) is written beside it.
Options: -OutDir (default build\<project name>), -Filter, -TimeoutSeconds (default 300) and -PBAutoBuild (the path to pbautobuild250.exe).
The text results look like this:
RESULT: FAIL
1 passed, 1 failed, 0 errors (2026-10-20 01:23:00)
PASS n_cst_pricing_tests.test_no_discount_below_threshold
FAIL n_cst_pricing_tests.test_discount_over_threshold
Price after discount: expected <90> but was <100>
| Exit code | Meaning |
|---|---|
| 0 | All tests passed |
| 1 | At least one test failed or had an error |
| 2 | Nothing ran or the run could not complete: build failure, no tests found, unknown argument, timeout |
The test exe can also be run directly, or from the PowerBuilder IDE, with these arguments:
| Argument | Default |
|---|---|
/out:<file> |
pbunit_results.txt |
/junit:<file> |
pbunit_results.xml |
/filter:<list> |
All tests. A comma-separated list of class or class.test names, where a trailing * matches a prefix. |
When running from the IDE, run the script once first so that n_pbunit_registry exists.
A real example: the PFC
The PowerBuilder Foundation Classes 2025 solution uses PBUnit 2025 for its unit tests (the pfc_tests project). Its first tests cover the DataWindow linkage service, including a regression test for a runtime error in of_RetrieveDetails (issue #8). Against the code before the fix, the test reports:
ERROR n_cst_dwsrv_linkage_tests.test_null_key_of_other_datatype_retrieves
Runtime error 40: Mismatched ANY datatypes in expression: string, double
at line 96 in function of_retrievedetails of object pfc_n_cst_dwsrv_linkage.
With the fix, it passes. The tests need no database: a u_dw descendant counts retrieve attempts in its retrievestart event and cancels them.
Things to know
- A test that opens a message box or modal window stops the run until the script's timeout. Configure the code under test to log instead of display, or replace the visual parts with test doubles.
- Test times are CPU time as reported by PowerBuilder's
CPU(), in milliseconds. - Windows Smart App Control blocks unsigned executables it has not seen before, which includes each newly built test exe. Sign the exe as part of the build, or run the tests on a machine where Smart App Control is off.
License
PBUnit is licensed under the Common Public License 1.0, and so is PBUnit 2025. The pbunit.pbl and pbunitfunc.pbl libraries come from the original project, with testcase extended with the new assertions. pbunitrunner.pbl, the scripts and the self-tests are new.
Download the source, report issues or contribute at https://github.com/bruce-armstrong/PBUnit2025.
Last Edit: 2 days 7 hours ago by Bruce Armstrong .
Please Log in or Create an account to join the conversation.