Concordion Java Fixtures: Patterns and Best Practices
The fixture class is where your Concordion spec connects to real application code. Getting the fixture structure right keeps your tests readable and maintainable as the spec grows. This post covers the patterns that work at scale.
Basic Fixture Setup
Every Concordion fixture class needs one annotation:
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class CheckoutTest {
public String totalWithTax(double subtotal, double taxRate) {
CheckoutService service = new CheckoutService();
return service.calculateTotal(subtotal, taxRate);
}
}The class name must match the spec file name. CheckoutTest.java maps to Checkout.html (or Checkout.md). The runner strips the Test suffix automatically.
Public methods on the fixture are callable from spec commands. Parameters are coerced from strings by Concordion: int, long, double, boolean, String, and their boxed variants all work without any extra configuration.
@ConcordionOptions
Use @ConcordionOptions to customize runner behavior per fixture:
import org.concordion.api.option.ConcordionOptions;
import org.concordion.api.option.MarkdownExtensions;
@RunWith(ConcordionRunner.class)
@ConcordionOptions(
declareNamespaces = {"ext", "urn:concordion-extensions:2010"},
markdownExtensions = {MarkdownExtensions.HARDWRAPS}
)
public class ReportTest {
// fixture methods
}Common uses:
declareNamespacesadds extension namespaces so you can use extension commands in the specmarkdownExtensionsadjusts Markdown parsing behavior when using.mdspecs
Partial Matches with Matchers
By default, concordion:assertEquals requires an exact string match. For cases where the exact output varies (timestamps, generated IDs, formatted numbers), use Concordion's Matchers:
import org.concordion.api.MultiValueResult;
import org.concordion.api.FullOGNL;
@RunWith(ConcordionRunner.class)
@FullOGNL
public class UserCreationTest {
public String createUser(String name, String email) {
User user = userService.create(name, email);
return user.getId(); // returns something like "USR-1234"
}
public boolean idStartsWith(String id, String prefix) {
return id.startsWith(prefix);
}
}In the spec:
<span concordion:assertEquals="idStartsWith(#userId, 'USR-')">true</span>For more sophisticated matching, return a MultiValueResult from your fixture method to assert multiple fields in one call:
public MultiValueResult userDetails(String email) {
User user = userService.findByEmail(email);
return MultiValueResult.multiValueResult()
.with("name", user.getName())
.with("role", user.getRole());
}Spec:
<span concordion:execute="#details = userDetails(#email)">details</span>
Name: <span concordion:assertEquals="#details.name">Alice</span>
Role: <span concordion:assertEquals="#details.role">admin</span>Before and After Suite with Extensions
For setup and teardown logic that runs once per test suite, implement the ConcordionExtension interface or use the AbstractCommand base class. The simpler approach for most teams is a JUnit @BeforeClass equivalent via the BeforeExample and AfterExample annotations:
@RunWith(ConcordionRunner.class)
public class DatabaseTest {
private static DataSource dataSource;
@BeforeExample
public void setUp() {
dataSource = TestDataSourceFactory.create();
dataSource.runMigrations();
}
@AfterExample
public void tearDown() {
dataSource.reset();
}
public int countRecords(String tableName) {
return dataSource.query("SELECT COUNT(*) FROM " + tableName);
}
}@BeforeExample and @AfterExample run before and after each example block in the spec, which aligns with how Concordion isolates test state.
Reusing Fixture Logic
When multiple specs share the same setup or helper methods, extract a base fixture class:
public abstract class BaseFixture {
protected final UserService userService = new UserService(TestConfig.dataSource());
protected final OrderService orderService = new OrderService(TestConfig.dataSource());
protected User createTestUser(String name) {
return userService.create(name, name.toLowerCase() + "@test.com");
}
protected void clearTestData() {
TestConfig.dataSource().executeScript("truncate-test-data.sql");
}
}Concrete fixtures extend it:
@RunWith(ConcordionRunner.class)
public class OrderTest extends BaseFixture {
public int orderCountFor(String userName) {
User user = createTestUser(userName);
return orderService.countFor(user.getId());
}
}Keep the base class free of @RunWith and any Concordion annotations. Only the concrete class gets the runner.
@FullOGNL for Complex Expressions
By default, Concordion uses a simplified expression evaluator. Add @FullOGNL to the fixture class to unlock the full OGNL expression language in specs. This lets you write expressions like #result.items.size() or #list[0].name directly in the spec without adding wrapper methods to the fixture:
@RunWith(ConcordionRunner.class)
@FullOGNL
public class CartTest {
public Cart addToCart(String productId, int quantity) {
return cartService.add(productId, quantity);
}
}Spec:
<span concordion:execute="#cart = addToCart(#productId, #qty)">add</span>
Item count: <span concordion:assertEquals="#cart.items.size()">1</span>Use @FullOGNL when it reduces noise in the fixture class. Avoid it if the expression logic becomes hard to read directly in the HTML.
Next Step
Add the ScreenshotExtension to your fixture so that failures capture a browser screenshot automatically. It is declared with a single annotation and requires no changes to the spec file. The extensions post covers the setup in detail.