Sub-Component Testing
This document outlines fluid APIs for testing assertions about the sub-components created by your layout specs. We make heavy use of AssertJ and Hamcrest. If you have used the two before, this should feel very familiar. If you haven't, don't worry. This guide will tell you everything there is to know.
In a rush? Jump straight to the Resources at the end of this page. More code, fewer words. Beep boop.
Prerequisites
The package is shipped as a separate module. It is available via maven as
com.facebook.litho:litho-testing
. To include it in your gradle build, add this
line to your dependencies
block:
testImplementation 'com.facebook.litho:litho-testing:0.36.0'
Basic Sub-Component Matching
Suppose you have a simple layout spec that truncates the text passed in at an arbitrary length and appends an ellipsis.
note
As a quick aside, please note that for trivial components like these it is often more appropriate to exploit the fact that these are pure functions that can be statically invoked. Whenever possible, test your business logic in isolation.
Now we want to test that the two cases the component is set out to handle are correctly implemented. Some people may prefer to write the tests before implementing the business logic, but technical documentation is not the place to start religious wars.
We start with telling JUnit to run our test with the ComponentTestRunner
.
Depending on your project setup, a vanilla RobolectricTestRunner
may also
suffice. Just ensure that your test runner correctly loads Yoga and the Android
platform libraries.
This JUnit rule serves two main purposes: It a) gives you easy access to a
ComponentContext
via getContext()
and b) registers improved representations
for Component
, Component.Builder
and LithoView
in AssertJ error messages.
As next step, we ensure that the tests are executed in debug mode.
This set of APIs makes use of features that are only available if the
IS_INTERNAL_BUILD
flag in ComponentsConfiguration
is enabled. This is
because we allocate some data structures for testing purposes that are
unnecessary in production builds. This means that this test suite will run for
:testDebugUnitTest
runs but will be skipped for :testReleaseUnitTest
.
Now to the actual test! Let's test first that short text (i.e. strings with fewer than 16 characters) is passed on unmodified.
Let's take a look at this in detail. LithoAssertions.assertThat
is an
overloaded static method that you can use in any place you would normally use a
standard AssertJ assertThat
. It provides all the normal bells and whistles but
adds matchers for various Litho-related types. For instance Component
and
LithoView
. If you want to assert on a constructed Component, you also need to
pass in the ComponentContext
that was used to build it.
Next, we call has()
on the assertion object. This is a standard AssertJ
method
and comes with the aliases is()
and are()
which can sometimes read nicer.
There are also negated forms: isNot()
, doesNotHave
and areNot()
.
These combinators all take a Condition
and this is exactly what
subComponentWith
creates for you. You give it a ComponentContext
and another
Condition
and it hands a composition of the two Conditions
back to you that
will succeed if it matches against at least one of the sub-components created by
your component.
In this case, we use textEquals
which ensures that there exists a Component
that renders the exact string given to it.
If we're honest, this should be enough for our base case, but let's overdo it a bit to showcase what AssertJ and Hamcrest allow us to do here:
If you see something like this in your code review, you would without a doubt reject it. And you should! But it does a pretty good job of illustrating how powerful the composition of matchers is.
To learn more about composing AssertJ condition, check out the official documentation.
We have to go deeper
Let's take a slightly more complex LayoutSpec this time. It still has the same text truncation logic, but adds some styling by wrapping the Text in a Column and a Card.
note
Time for another quick aside to remind you that this is a contrived
example and it would be way better to keep our previous
TruncatingComponentSpec
around and use this LayoutSpec to wrap it. But
tutorials - and sometimes the real world - can be stinkers.
The setup boilerplate will look just like in our previous example.
When testing components, we care about the business logic and not the styling of it, so will focus on sub-component containing the text again. Let's start with a naïve attempt by just copying our previous test:
So I've got good and bad news for you. The bad news first: This test fails. However, the good news is that the error message gives us a good hint as to why:
As you can see from the output, the "Text" we're looking for
sits nested in the hierarchy. This means that our subComponentWith
matcher
won't work here.
Luckily enough, there is another matcher just for this case:
deepSubComponentWith
. So, let's just replace it in our assertion:
And boom! Our build is back to green. Everything else remains exactly the same.
We can still use various combinators as well as mix and match with allOf()
and
anyOf
if we so desire.
DIY Conditions
If you look at
ComponentConditions
you can find a growing collection of matchers that can be used in
subComponentWith
and deepSubComponentWith
. But don't let the existing list
limit your creativity! Writing your own conditions is a piece of cake.
All you need to do to write your own condition is implement the
Condition<InspectableComponent>
interface which consists of a single method:
matches(InspectableComponent)
.
InspectableComponent
is a wrapper around a Component
with additional information we have about the
component at test time.
Let's implement a custom matcher that asserts that a given Component has a background:
Nothing here should come as a surprise, but it may be worth noting that the as
invocation instructs AssertJ to record the use of this condition in a potential
error message as you have seen it above.
If we want to put this to use, we use it in the same place we previously made our text assertions:
Simple Matching with SubComponent.of
For very simple cases, it can sometimes be enough to verify the mere presence of a sub-component of a certain class. For these cases, we have the SubComponent.of API which works without any code generation.
In the following example, we use the hasSubComponents
method on the AssertJ
matcher to ensure that the Components of the given types are either present of absent.
Old Meets New
The example above may look a bit foreign as it doesn't work with has
and is
thus not composable with other matchers. We provide a bridge interface so you
can combine the best from the old and new world of sub-component testing with
legacySubComponent.
It accepts an existing SubComponent
and works with both shallow and deep
sub-component traversals. This is great if you want to ensure that a property
with a given set of props exists in the component tree.
Next
Either head back to the testing overview or continue with the next section, Matching Props.
Resources
To learn more, check out these resources: