Fumapps Testing Methodology: Sociable BDD with Internal DSLs
At Fumapps, we are always searching for the sweet spot in software testing—balancing test suite execution speed, maintainability, and developer clarity. To achieve this, we developed a combined testing methodology that we call Sociable BDD with Internal DSLs (also referred to as Declarative Component BDD).
This methodology synthesizes four established software testing and design patterns into a unified, developer-friendly workflow:
- Sociable Unit Testing for robust integration verification.
- Behavior-Driven Development (BDD) structure for clear intent.
- Composed Methods to keep test scenarios readable.
- Internal Domain-Specific Languages (DSLs) to verify complex state spaces.
Here is an in-depth look at how we combine these patterns and how they work together in our codebase.
The Core Concept: Defragmenting Your Tests
Classic mock-heavy unit tests focus on absolute isolation. While this isolates failures, it often results in brittle test suites that break during simple internal refactorings.
Our combined approach shifts the focus: we test clusters of collaborating components as a single unit, using human-readable specifications.
- Sociable Unit Testing: Instead of mocking every collaborator, we test against a facade or a logical component group. Collaborators are real (“sociable”), not doubled. If the internal structure changes but the behavior remains correct, the test stays green.
- BDD (Given-When-Then) Structure: We structure tests strictly around behaviors rather than class methods. The scenario defines a state (Given), triggers an action (When), and asserts outcomes (Then).
- Composed Methods: To keep the test case clean and readable, we hide setup and action details behind descriptive helper methods. The main test body reads like a clear, high-level script.
- String-Based Internal DSLs: For complex data structures (like maps, trees, or multi-button states), standard assertions become verbose and unreadable. We use lightweight string DSLs to visually assert the entire state in a single, glanceable block.
Practical Case Study: The Java Hamster Simulator
To demonstrate this approach in action, let’s look at a unit test from the open-source Hamster Simulator (developed at the Institute of Software Technology at the University of Stuttgart for teaching programming).
By applying our methodology, we can write a highly expressive test that verifies the simulator’s grid and controls:
@Test
public void testHamsterView() throws IOException {
// Given
loadTerritory("example01.ter");
// When
moveHamster();
// Then
assertTerritory(
"|####|####|####|\n" +
"| |> | 1* |\n" +
"|####|####|####|\n"
);
assertButtons("/play/ [pause] /undo/ /redo/");
}
Pattern Deconstruction in the Code
- Composed Method Pattern: The setup details (
loadTerritory) and actions (moveHamster) are encapsulated in helper methods. This prevents boilerplate from obscuring the test logic. - Sociable Unit Testing Pattern: The test doesn’t mock the internal hamster controller, movement engine, or territory model. It loads the actual system, moves the hamster, and verifies the resulting state of all cooperating classes.
- Internal DSL Pattern:
assertTerritoryparses a visual representation of the grid.####represents walls,>shows the hamster facing east, and1*represents a grain on the tile.assertButtonsvalidates the active state of UI buttons in a single string format (e.g., active commands are/slashes/, disabled commands are[brackets]).
Critical Assessment & Trade-offs
Every architectural choice has trade-offs. Here is how we evaluate our combined methodology:
The Advantages
- High Refactoring Safety: Since the test only exercises the public facade and asserts behavior, you can completely rewrite the internal class structure without modifying a single line of test code.
- Instant Readability: When a test fails, the diff of the string-based DSL instantly reveals what went wrong. You don’t have to trace through variables or nested lists.
- Living Documentation: The test acts as a clear, executable specification of the domain requirements that any developer can understand at a glance.
The Disadvantages
- Loss of Defect Localization: When
testHamsterViewfails, it could be due to a bug in the movement logic, the map loader, or the button controller. Pinpointing the root cause requires slightly more debugging than in isolated unit tests. - The “Mystery Guest” Anti-Pattern: Relying on external files like
example01.tercan obscure what the initial state is. Keep your setup files small and well-documented. - Loss of Type Safety & IDE Support: Refactoring tools won’t rename variables inside your DSL strings. Autocomplete is also unavailable when writing DSL assertions. For large systems, this requires extra discipline or investment in custom test tooling.
Conclusion
At Fumapps, we found that combining Sociable Unit Testing with Internal DSLs yields highly maintainable test suites for complex state machines. By focusing on behavior instead of implementation details, our tests remain stable during refactoring and serve as the ultimate documentation of our codebase.