Most AL developers have heard the word “mock,” but a mock is only one of five kinds of test doubles. In this webinar, Vjekoslav Babic (vjeko.com), moderated by Luc van Vugt, presents dummies, stubs, spies, fakes, and mocks, and shows each of them in real AL test code. You’ll learn how to identify the dependencies hiding in your code, decouple them, and pick the right kind of test double so your tests run faster, stay focused, and actually validate the thing you meant to test.
Why dependency-heavy code is hard to test
Babic opens with a piece of standard Business Central sales posting code: two lines that email a posted invoice. Writing a meaningful test for code like this runs into several obstacles at once. The test environment may not have email sending configured correctly, sending mail is slow, you don’t actually want to send email while running tests, the test becomes fragile because it depends on a component you don’t control, and you can’t easily simulate failure scenarios such as an unreachable mail server.

The “grab-and-handle” anti-pattern
Many AL developers already write a crude form of test double without realizing it: subscribing to an event like OnBeforeEmailRecords and setting IsHandled to true so the real logic never runs. Babic calls this a “grab-and-handle double.” It solves the performance and side-effect problems, but it does nothing to make the test resilient to failure scenarios or to verify that the dependency was even invoked correctly. He compares it to a car safety test where you simply remove the crash-test dummy from the street: the car will never register a collision, but the test tells you nothing about what you actually needed to know.

Why use test doubles at all
Babic lists five reasons to reach for test doubles instead of real dependencies:
- Isolation – verify that the code under test behaves correctly without being affected by its dependencies.
- Performance – doubles need far less setup and no database round-trips, so tests run faster and can be run more often.
- Repeatability – a double can consistently return the same result, unlike a real process that might generate a different posted document number every run.
- Control – doubles let you simulate edge cases and failures, such as an unreachable host or an invalid account, that are difficult to reproduce with real dependencies.
- Flexibility – a double can be as simple or as complex as the specific test requires.
A naive test, and why it falls short
To illustrate the problem, Babic walks through a small “fancy” currency conversion process: check permissions, perform the conversion, then log the operation. Written the way most AL code still gets written today, all three steps live in one function. The corresponding test has to insert two currencies, an exchange rate, and a permission record just to exercise one path through the code.
He calls this style “naive testing,” and lists what’s wrong with it: it requires a lot of setup, it produces a lot of redundant, copy-pasted test code across scenarios, it isn’t really a unit test (it exercises three independent things at once), it depends heavily on the database, it runs slowly, and – most ironically – large parts of it validate standard Business Central behavior that Microsoft has already tested rather than the developer’s own logic.
Identifying and decoupling dependencies
The fix starts with identifying dependencies. Babic’s rule of thumb: if a piece of code can be extracted into an independent component, can be reused from multiple places, can be swapped for an equivalent implementation without breaking the process, or can be treated as a black box, it’s a dependency. In his example, the permission check, the currency conversion, and the logging call are all separate dependencies of the process.
Business Central already has several patterns for decoupling dependencies – facades, variant facades, the discovery pattern, the handled pattern – but Babic’s preferred approach for this session is interfaces. He extracts an ILogger interface and implements it in a Database Logger codeunit that mirrors what the original inline code did.

📖 Docs: Interfaces in AL – Microsoft’s reference on declaring and implementing interfaces, the mechanism Babic uses throughout the webinar to decouple dependencies.
Once dependencies are behind interfaces, the code needs an inversion-of-control mechanism so it receives its dependencies from the outside instead of deciding on them itself. Babic uses a factory codeunit that can return either the real implementation or a test double for the logger, permission checker, and currency converter. After refactoring, the Convert function takes the factory as a parameter and no longer hard-codes which implementation it talks to.
The five types of test doubles
With the dependencies decoupled, Babic works through the five types of test doubles defined by Gerard Meszaros in his 2007 book xUnit Test Patterns.
📖 Reference: Test Double – xUnitPatterns.com – Meszaros’ original definitions of dummy, fake, stub, spy, and mock objects, the classification this webinar is built around.
Dummy
A dummy does nothing at all. It’s passed around purely to satisfy the requirement that a dependency be present – for example, passing a dummy currency code into a test that only checks whether an invalid code is rejected. A “grab-and-handle” subscriber is, in Babic’s terms, really just a dummy.

Stub
A stub returns a predefined, fixed state to whatever consumes it. Babic’s example is a StubPermissionChecker that can be configured to answer “allowed” or “disallowed,” letting a test exercise both branches of the permission check without writing anything to the database.
Spy
A spy is a dummy or stub that also records how and when it was used. Babic’s SpyLogger records whether its Log function was invoked, so a test can assert that the conversion process called the logger without caring how the logger itself behaves.

With dummy, stub, and spy doubles for the permission checker, converter, and logger in place, the al-test-doubles repository’s second-stage tests need no database setup at all for most scenarios – only the tests that specifically validate the database-backed permission checker and logger still touch the database.
Fake
A fake is a working implementation that takes shortcuts but still performs real work – it just doesn’t behave exactly like the real system. Babic’s example: a sales process that depends on currency conversion but doesn’t care how the conversion happens, only that it succeeds for known currency pairs and fails for unknown ones. A fake currency converter can produce that behavior without touching the database at all.

Mock
A mock is a more elaborate fake, pre-programmed with specific expectations, typically used when there are multiple dependency layers and the behavior under test depends directly on the configuration of the layer beneath it. In the final demo, Babic builds a REST-based currency converter that depends on an IHttpInvoker interface, then creates several mock invokers – one that simulates a 401 response, one that returns invalid JSON, one that’s blocked by the environment, and one that succeeds – to verify the converter handles every failure mode correctly, without making a single real HTTP call.

Running all three stages side by side in the demo – the naive, database-driven tests; the dummy/stub/spy-based tests; and the mock-based HTTP tests – shows the doubles-based tests completing multiple times faster, since only the tests that specifically target the database-backed permission checker and logger still need the database at all.
Where AL makes it complicated
Doubles aren’t always straightforward to apply in AL. The database is the most common dependency other platforms fake or mock outright, but that’s not realistically an option in Business Central. The base application is also a large, mostly legacy-structured dependency that every extension relies on, and much of it wasn’t written with testability in mind.

Babic’s recommendations for making AL code more testable:
- Add layers of abstraction – wrap calls to the base app in isolated components behind interfaces instead of calling it directly.
- Wrap database write operations into isolated, interface-backed methods.
- Make functions pure whenever possible – have all input come from parameters and remove dependency on global state.
- Pass record parameters instead of code parameters into application code – a passed
Codeforces aGetthat can’t be substituted with a double, while a passed record can be replaced with a dummy in test code.
Takeaways

- Application code written with test doubles in mind is more modular and generally better structured.
- Test doubles give you superior performance and full control over test conditions.
- Different types of test doubles serve different purposes – know which one you actually need.
- Test doubles allow isolated testing of individual components while still enabling reliable integration tests.
Q&A: one double per object, or one per responsibility?
Asked whether AL developers, who have to implement test doubles manually, should combine multiple double behaviors (stub and spy, for example) into a single object rather than writing separate implementations, Babic compared it to the same question in application code: do you split business logic into multiple codeunits, or stuff everything into one large management codeunit? His preference is to keep doubles separate – one stub, one spy, one fake per responsibility – because it stays easier to follow and maintain months later, even though object ID and license constraints sometimes force a more consolidated approach.
Resources
💻 Demo repository: github.com/vjekob/al-test-doubles – the full AL project from the webinar, including the naive test code, the dummy/stub/spy-based tests, and the mock-based HTTP tests, with a README walking through each stage.
🌐 Presenter’s site: vjeko.com – Vjekoslav Babic’s blog on AL and Business Central development.
This post was drafted with AI assistance based on the webinar transcript and video content.
