Concordion Tutorial: Acceptance Testing with Living Specifications
Concordion is a Java testing framework that turns plain-English specifications into executable acceptance tests. Unlike Cucumber's Gherkin syntax, Concordion embeds test instrumentation directly into HTML documents — the same documents your product team writes. The result is specifications that can be run as tests without any transformation.
What Makes Concordion Different
Most BDD frameworks require you to translate between two formats: the spec (what the business writes) and the test (what engineers execute). Concordion collapses that gap. Your HTML specification document is the test. Developers instrument the HTML with Concordion commands, link it to Java fixture code, and the framework executes the document itself.
When tests pass, Concordion renders the spec with green highlights. When they fail, you see exactly which assertions broke — directly in the document your team already reads.
Project Setup
Add the Concordion dependency to your Maven project:
<dependency>
<groupId>org.concordion</groupId>
<artifactId>concordion</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>For Gradle:
testImplementation 'org.concordion:concordion:2.2.0'Concordion requires JUnit 4 or JUnit Vintage runner. If you're on JUnit 5, add the vintage engine:
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>Writing Your First Specification
Create an HTML file in src/test/resources that mirrors your fixture class package. For com.example.UserRegistration, create src/test/resources/com/example/UserRegistration.html:
<html xmlns:concordion="http://www.concordion.org/2007/concordion">
<body>
<h1>User Registration</h1>
<p>
When a user registers with email
<span concordion:set="#email">alice@example.com</span>
and password
<span concordion:set="#password">SecurePass123!</span>,
the system should
<span concordion:assertEquals="register(#email, #password)">succeed</span>.
</p>
<h2>Email Validation</h2>
<table concordion:execute="#result = validateEmail(#email)">
<tr>
<th concordion:set="#email">Email</th>
<th concordion:assertEquals="#result">Valid?</th>
</tr>
<tr>
<td>user@example.com</td>
<td>true</td>
</tr>
<tr>
<td>not-an-email</td>
<td>false</td>
</tr>
<tr>
<td>user@</td>
<td>false</td>
</tr>
</table>
</body>
</html>Writing the Fixture Class
The fixture class connects your HTML specification to your application code:
package com.example;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class UserRegistrationFixture {
private UserRegistrationService service = new UserRegistrationService();
public String register(String email, String password) {
try {
service.register(email, password);
return "succeed";
} catch (RegistrationException e) {
return "fail: " + e.getMessage();
}
}
public boolean validateEmail(String email) {
return service.isValidEmail(email);
}
}The fixture class name matches the HTML file name. Concordion finds the HTML document automatically based on the class location.
Concordion Commands
Concordion provides a small, focused set of commands:
concordion:set — assigns a value to a variable:
<span concordion:set="#username">alice</span>concordion:assertEquals — calls a fixture method and asserts the result:
<span concordion:assertEquals="greet(#username)">Hello, alice!</span>concordion:execute — calls a fixture method without asserting the return value:
<span concordion:execute="login(#username, #password)"/>concordion:assertTrue / concordion:assertFalse — asserts a boolean:
<span concordion:assertTrue="isLoggedIn()">the user is logged in</span>concordion:verifyRows — validates a table of expected results:
<table concordion:verifyRows="#order : getOrders()">
<tr>
<th concordion:assertEquals="#order.id">Order ID</th>
<th concordion:assertEquals="#order.total">Total</th>
</tr>
</table>Table-Driven Acceptance Tests
Tables are where Concordion shines. Suppose you're specifying discount calculation rules:
<table concordion:execute="#discount = calculateDiscount(#orderTotal, #customerType)">
<tr>
<th concordion:set="#orderTotal">Order Total</th>
<th concordion:set="#customerType">Customer Type</th>
<th concordion:assertEquals="#discount">Discount %</th>
</tr>
<tr><td>50</td><td>standard</td><td>0</td></tr>
<tr><td>100</td><td>standard</td><td>5</td></tr>
<tr><td>100</td><td>premium</td><td>10</td></tr>
<tr><td>500</td><td>premium</td><td>15</td></tr>
</table>This table is both documentation and an executable test. Your product team can read it. Your CI pipeline can run it.
Specification Hierarchy with Includes
For large specifications, use the concordion:run command to compose multiple specs:
<!-- Main spec: OrderProcessing.html -->
<a concordion:run="concordion" href="Inventory.html">Inventory management</a>
<a concordion:run="concordion" href="Payment.html">Payment processing</a>
<a concordion:run="concordion" href="Shipping.html">Shipping rules</a>Each linked spec runs independently. Failures in sub-specs show as failures in the parent.
Running Tests and Reading Output
Run tests with Maven:
mvn testConcordion generates an HTML report in target/concordion/. Open it in a browser. Passing assertions are green, failing ones are red, with full detail on what was expected versus what the fixture returned.
The output HTML is your living documentation — a snapshot of what the system does, verified by tests that just passed.
Best Practices
Keep specifications business-readable. If the HTML reads like a technical spec, rewrite it. Concordion's value is documentation that non-engineers can understand.
Separate fixture logic from production code. Fixtures are test infrastructure — keep them thin. Call production service classes directly; don't put business logic in fixtures.
One concept per specification. A 200-line spec document is too long. Split into multiple files, linked with concordion:run.
Use concordion:set for setup, tables for data-driven cases. Narrative prose handles the happy path; tables handle boundary conditions and variations.
Version control your specs. HTML specifications live in src/test/resources alongside your tests. They change when requirements change — treating them like test code means they stay accurate.
Connecting to Continuous Integration
Concordion tests run as standard JUnit tests. Any CI system that runs JUnit can run Concordion:
# GitHub Actions
- name: Run Concordion tests
run: mvn test -Dtest="*Fixture"Publish the Concordion HTML output as a CI artifact for stakeholder review:
- name: Upload Concordion reports
uses: actions/upload-artifact@v3
with:
name: concordion-reports
path: target/concordion/Adding Continuous Monitoring
Concordion verifies system behavior through acceptance tests. For ongoing production assurance, pair it with HelpMeTest — which runs your critical user flows 24/7 in plain English without requiring Java or a build pipeline. Use Concordion to specify and verify behavior during development; use HelpMeTest to confirm that behavior stays correct in production.
Summary
Concordion turns HTML specifications into executable acceptance tests. The framework is intentionally minimal — a handful of HTML attributes, a Java fixture class, and a JUnit runner. The output is living documentation: spec documents annotated with pass/fail evidence from real test runs. If you're working in Java and want specifications that non-technical stakeholders can write and read, Concordion is worth adding to your toolkit.