Getting Started
To use any of the testing utilities, include the litho-testing
package in your build.
Add the following lines to the dependencies
block in the build.gradle
file:
testImplementation 'com.facebook.litho:litho-testing:0.50.2'
Litho's testing APIs are exposed through fluid AssertJ methods and are available as:
- ComponentAssert for assertions that are run against either Component builders or Components.
- LithoViewAssert for assertions against mounted UI hierarchies.
For convenience, LithoAssertions.assertThat can be statically imported (it hosts all the APIs of ComponentAssert
,LithoViewAssert
, and ListAssert<Component>
):
import static com.facebook.litho.testing.assertj.LegacyLithoAssertions.assertThat;
Exampleβ
To demonstrate how the APIs are used, consider the following component that displays a 'like' icon and a short description:
/**
* Displays who liked the post.
*
* 1 - 3 likers => Comma separated names (example: Jane, Mike, Doug)
* > 3 likers => Comma separated number denoting the like count
*/
@LayoutSpec
class LikersComponentSpec {
@OnCreateLayout
protected static Component onCreateLayout(
ComponentContext c,
@Prop List<User> likers) {
return Row.create(c)
.alignItems(FLEX_START)
.child(
Image.create(c)
.drawableRes(R.drawable.like))
.child(
Text.create(c)
.text(formatLikers(likers))
.textSizeSp(12)
.ellipsize(TruncateAt.END))
.build();
}
private static String formatLikers(List<User> likers) {
...
}
}
Setupβ
To verify the rendering of the text and the icon.
- Create a new test class;
LikersComponentTest
. - Add
@RunWith(RobolectricTestRunner.class)
to the top of the test class. - Add a
LithoTestRule
JUnit@Rule
that sets up overrides for Styleables and exposes some useful APIs.
The test class should look like the following:
@RunWith(RobolectricTestRunner.class)
public class LikersComponentTest {
public final @Rule LithoTestRule mLithoTestRule = new LithoTestRule();
}
Testing Component Renderingβ
LegacyLithoAssertions
exposes AssertJ-style APIs to assert what gets rendered by a component. These APIs will generally layout, mount, and render the component before testing the assertions:#
@RunWith(RobolectricTestRunner.class)
public class LikersComponentTest {
public final @Rule LithoTestRule mLithoTestRule = new LithoTestRule();
@Test
public void whenTwoUsersLike_shouldShowBothUserNames() {
final ComponentContext c = mLithoTestRule.getContext();
final ImmutableList<User> likers = ImmutableList.of(
new User("Jane"), new User("Mike")
);
final LikersComponent component = LikersComponent.create(c)
.likers(likers)
.build();
LegacyLithoAssertions.assertThat(c , component).hasVisibleText("Jane, Mike");
}
@Test
public void whenUsersLike_shouldShowLikeIcon() {
final ComponentContext c = mLithoTestRule.getContext();
final ImmutableList<User> likers = ImmutableList.of(
new User("Jane"), new User("Mike")
);
final LikersComponent component = LikersComponent.create(c)
.likers(likers)
.build();
final Drawable likeIcon = c.getResources().getDrawable(R.drawable.like);
LegacyLithoAssertions.assertThat(c , component).hasVisibleDrawable(likeIcon);
}
}
Additional Assertionsβ
There are several additional assertions that can be tested using LegacyLithoAssertions. To see the entire scope of APIs, refer to the JavaDoc LithoAssertions.
Those APIs test assertions on the view hierarchy that's created by the mounted Component. So, asserting the presence of a Drawable in a Component will traverse the entire view hierarchy rendered by the Component.
Following are some of the assertions provided by LegacyLithoAssertions:
LegacyLithoAssertions#hasVisibleTextMatching()
LegacyLithoAssertions#doesNotHaveVisibleText()
LegacyLithoAssertions#willRender()
LegacyLithoAssertions#doesNotHaveVisibleDrawable()
LegacyLithoAssertions#hasContentDescription()
LegacyLithoAssertions#willNotRender()
When running Litho unit tests, be aware that the native library for Yoga must be loaded, which can pose some challenges depending on your build system of choice. With Gradle and Robolectric, for instance, you may run into issues as Robolectric spins up new ClassLoaders for every test suite with a different configuration. The same applies to PowerMock, which prepares the ClassLoaders on a per-suite basis and leaves them in a non-reusable state.
The JVM has two important and relevant limitations:
- A shared library can only ever be loaded once per process.
ClassLoader
s do not share information about the libraries loaded.
Due to these limitations, using multiple ClassLoaders for test runs is highly problematic as every instance will attempt to load Yoga. All, except the first, calls will fail with the libyoga.so already loaded in another classloader
exception.
The only way to avoid the limitations is by preventing the use of multiple ClassLoaders or forking the process whenever a new ClassLoader is necessary.
Gradle allows you to limit the number of test classes a process can execute before it is discarded. If you set the number to one, you avoid the ClassLoader reuse:
android {
[...]
testOptions {
unitTests.all {
forkEvery = 1
maxParallelForks = Math.ceil(Runtime.runtime.availableProcessors() * 1.5)
}
}
}
With Buck, this behaviour can be achieved by assigning test targets separate names, which results in a parallel process being spun up. Alternatively, you can
set the fork_mode
to per_test
, as described in the java_test page of the Buck website.
Ultimately, depending on your build system and the existing constraints of your project, you may need to adjust the way in which your test runner utilises ClassLoaders. This is not a problem unique to Litho, it's an unfortunate consequence of mixing native and Java code in Android projects.