Sub-Component Testing
The Getting Started page contains information to help you setup your test environment.
This page outlines APIs for testing assertions about the component hierarchy.
Litho's testing APIs are similar to the APIs of AssertJ and Hamcrest. If you have used those two before, the content of this page should be very familiar.
Basic Sub-Component Matchingβ
To demonstrate the use of the Litho testing APIs, consider the following component that truncates passed text and appends an ellipsis:
@LayoutSpec
class TruncatingComponentSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c, @Prop String text) {
// A unicode-aware implementation is left as an exercise to the reader.
final String s = text.length() > 16 ? text.substring(0, 16) + "..." : text;
return Text.create(c).text(s).build();
}
}
For trivial components, it is often more appropriate to exploit the fact that they are pure functions that can be statically invoked. Whenever possible, test your business logic in isolation.
Setupβ
- Add
@RunWith(LithoTestRunner.class)
to the top of the test class. - Add a JUnit
@Rule
LithoViewRule
. - Add a check to ensure that tests are run in debug mode.
ComponentsConfiguration.IS_INTERNAL_BUILD
must be true.
The test class should look like the following:
/**
* Tests {@link LikersComponent}
*/
@RunWith(RobolectricTestRunner.class)
public class TruncatingComponentTest {
public final @Rule LithoViewRule mLithoViewRule = new LithoViewRule();
@Before
public void assumeInDebugMode() {
assumeThat(
"These tests can only be run in debug mode.",
ComponentsConfiguration.IS_INTERNAL_BUILD, is(true)
);
}
}
Testing Assertion on the Component Hierarchyβ
@Test
public void whenTextLengthIsLessThan16_shouldContainTextComponentWithFullText() {
final ComponentContext c = mLithoViewRule.getContext();
final TruncatingComponent component = TruncatingComponent.create(c)
.text("Mr. Meeseeks").build();
LegacyLithoAssertions.assertThat(c, component)
.has(subComponentWith(c, textEquals("Mr. Meeseeks")));
}
Understanding the API Usageβ
LegacyLithoAssertions.assertThat(ComponentContext, Component)
creates and mounts the layout.has(ComponentContext, Condition)
tests if the component hierarchy passes the assertion of the Condition. This is a standard AssertJ API; to see all AssertJ APIs, refer to the AssertJ Core Conditions website.subComponentWith(ComponentContext, Condition)
is a utility method from Litho's testing APIs to compose Conditions together.textEquals(String)
is another utility method that creates a Condition that passes only for a Component of typeText
, which has its text property set to the String argument.subComponentWith(c, textEquals("Mr. Meeseeks")
creates a Condition that 'passes for a component of type Text with its text property set to "Mr. Meeseeks"'.
The following code is a more complex composition of similar assertions:
LegacyLithoAssertions.assertThat(c, component)
.has(allOf(
subComponentWith(c, textEquals("Mr. Meeseeks")),
subComponentWith(c, text(startsWith("Mr."))),
subComponentWith(c, anyOf(
text(endsWith("Sanchez")),
text(containsString("Mees")))
)
));
The above assertions are only for the purpose of illustration. This is not a good test!
Matching Against Complex Hierarchiesβ
Consider the following more complex LayoutSpec. It still has the same text truncation logic, with some new UI elements, and wraps the Text in a Column and a Card.
@LayoutSpec
public class TruncatingComponentSpec {
@OnCreateLayout
public static Component onCreateLayout(ComponentContext c, @Prop String text) {
final String s = text.length() > 16 ? text.substring(0, 16) + "..." : text;
return Column.create(c)
.backgroundColor(Color.GRAY)
.child(Card.create(c).content(Text.create(c).text(s)))
.build();
}
}
The original test will start failing now:
@Test
public void whenTextLengthIsLessThan16_shouldContainTextComponentWithFullText() {
final ComponentContext c = mLithoViewRule.getContext();
final TruncatingComponent component = TruncatingComponent.create(c).text("Mr. Meeseeks").build();
LegacyLithoAssertions.assertThat(c, component)
.has(subComponentWith(c, textEquals("Mr. Meeseeks")));
}
The error messages should provide sufficient information to understand why the test failed. The error message prints out the component hierarchy, and the assertion that failed:
Expecting:
<TruncatingComponent{0, 0 - 100, 100}
Card{0, 0 - 100, 6}
Column{3, 2 - 97, 2}
Text{0, 0 - 94, 0 text="Szechuan Sauce"}
CardClip{0, 0 - 94, 0}
CardShadow{0, 0 - 100, 6}>
to have:
<sub component with <text <is "Szechuan Sauce">>>
Here, the Text
component was expected to be a direct descendant of TruncatingComponent
. However, the error message shows that the Text component is several levels below the TruncatingComponent.
This test can be fixed by using a different Condition API called deepSubComponentWith
. As the name suggests, this condition will test against all the components in the hierarchy, and not just the immediate descendant.
@Test
public void whenTextLengthIsLessThan16_shouldContainTextComponentWithFullText() {
final ComponentContext c = mLithoViewRule.getContext();
final TruncatingComponent component = TruncatingComponent.create(c).text("Mr. Meeseeks").build();
LegacyLithoAssertions.assertThat(c, component)
.has(
deepSubComponentWith(c, textEquals("Mr. Meeseeks"))
);
}
For information on all Component conditions, see the ComponentConditions JavaDoc.
Custom Conditionsβ
Custom Conditions can be created by implementing the Condition<InspectableComponent>
interface which consists of a single method: matches(InspectableComponent)
.
InspectableComponent is a wrapper around a Component
with additional information about the component.
Creating a Custom Conditionβ
public static Condition<InspectableComponent> hasBackground() {
return new Condition<InspectableComponent>() {
@Override
public boolean matches(InspectableComponent value) {
as("any background"); // error message.
return value.getBackground() != null; // value contains the component being tested.
}
};
}
Using a Custom Conditionβ
@Test
public void whenTruncatingComponentIsRendered_shouldHaveBackground() {
final ComponentContext c = mLithoViewRule.getContext();
final TruncatingComponent component = TruncatingComponent.create(c).text("Mr. Meeseeks").build();
LegacyLithoAssertions.assertThat(c, component)
.has(deepSubComponentWith(c, hasBackground()));
}
Simple Sub-Component Matchingβ
To test for the mere presence of a component of a certain type, use the SubComponent.of API.
Consider a hypothetical LayoutSpec called StoryComponentSpec
, which consists of a HeaderComponentSpec
, MessageComponentSpec
, LikersComponentSpec
, and a
FeedbackComponentSpec
.
The following test can be used to assert the presence of those components in the hierarchy:
public class StoryComponentTest {
...
@Test
public void whenStoryComponentIsRendered_shouldContainAllSubcomponents() {
final ComponentContext c = mLithoViewRule.getContext();
final StoryComponent.Builder builder = StoryComponent.create(c).build();
assertThat(builder).hasSubComponents(
SubComponent.of(HeaderComponent.class),
SubComponent.of(MessageComponent.class),
SubComponent.of(LikersComponent.class),
SubComponent.of(FeedbackComponent.class));
assertThat(builder)
.doesNotContainSubComponent(SubComponent.of(TruncatingComponent.class));
}
}
Using with Legacy APIsβ
Litho provides a bridge interface legacySubComponent that enables use of the SubComponent.of
API with the subComponentWith
APIs. It accepts a SubComponent
and works with both shallow and deep sub-component traversals. This is ideal if you want to ensure that a component with a given set of props exists in the component tree.
@Test
public void testSubComponentLegacyBridge() {
final ComponentContext c = mLithoViewRule.getContext();
assertThat(c, mComponent)
.has(
subComponentWith(
c,
legacySubComponent(
SubComponent.of(
FooterComponent.create(c).subtitle("Gubba nub nub doo rah kah").build()))));
}
Resourcesβ
To learn more, see the following resources: