Introduction

It's generally easier to test components than pages because they have less dependencies and generally you need to mock, fake or stub all the dependencies of the thing you are testing. I find that I break complex pages up into components in order to ease testing, which has the side effect of making the code easier to understand. It's similar to the effect when you do test-driven development where code becomes broken up into more smaller classes that are easier to unit test.

Components can't be rendered directly - they are always part of another component or a page. So, to test a component you must create a demo page that contains the component (so-called because they demonstrate the component's use). You then write tests that render the demo page and assert conditions about the content of that page.

Writing demo pages

I create demo pages in a parallel hierarchy near the test package. So if the components are in myproject.tapestry.components then the tests will be in test.myproject.tapestry.components and the demo pages will be in test.myproject.tapestry.demo.pages.

This choice makes the demo pages and the tests fairly near each other in a package explorer view in your IDE. You can't put tests and demo pages in the same package because of T5's special classloading for pages/components/mixins.

To make Tapestry aware of the demo pages, you must create the IOC container with an additional module (give this class as a parameter when you create your TapestryTester):

package test.myproject.tapestry.demo;

public class DemoModule {
    public static void contributeComponentClassResolver(
        Configuration<LibraryMapping> configuration) {
        configuration.add(new LibraryMapping("demo", DemoModule.class.getPackage().getName()));
    }
}

And, in your tests, render the demo page like this (notice the demo/ prefix at the start):

private Document renderPage() {
   return tester.renderPage("demo/DayMonthYearDateInputDemo");
}

Here is an overview of the package structure and component, test and demo page files:

 +---src
     +---main
     |   +---java
     |       +---myproject
     |           +---tapestry
     |               +---components
     |                       MyComponent.java
     |                       MyComponent.tml
     |
     +---test
         +---java
             +---test
                 +---myproject
                     +---tapestry
                         +---components
                         |       MyComponentTest.java
                         |
                         +---demo
                             |   DemoModule.java
                             |
                             +---pages
                                     MyComponentDemo.tml
                                     MyComponentDemo.java

Mocking, stubbing, faking services in components

It's easy to substitute the services that a component uses, just create fields in your test with a @ForComponents annotation. See the page describing how to inject objects from the test into the component for full details.

Passing test values as component parameters

Components often have parameters and tests will often need to set the parameters to different values. This means that the test must define a value that the demo page can pick up and then pass to the component. Tests and pages can't communicate directly, however the test can define fields with the @ForComponents annotation and the demo page can pick them up with @Inject. Note that you must use a name in the @ForComponents and a @Service annotation in the demo page (this is to stop conflicts with other kinds of injection - for example, by default in Tapestry, injecting a String injects the component Id).

Here's an example:

-- MyComponentDemo.tml
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
    <body>
        <div id="message"><t:mycomponent myParam="theParamValue"/></div>
    </body>
</html>
-- MyComponentDemo.java
public class MyComponentDemo {
    @Inject
    @Service("valueForMyParam")
    @Property
    private String theParamValue;
}
-- MyComponentTest.java
public class MyComponentTest extends AbstractMyApplicationTest {
    @ForComponents("valueForMyParam")
    private String theParamValue;
}