Compose Testing Advanced 02: Custom Matchers and Assertions 🧩

EspressoLab.Ai
3 min readJun 19, 2024

--

Introduction

Welcome to the second installment of our advanced series on Compose Testing! In this post, we’ll explore the creation and usage of custom matchers and assertions in Jetpack Compose. Custom matchers and assertions are powerful tools that can simplify complex test conditions and make your tests more readable and maintainable.

Why Use Custom Matchers and Assertions?

While Compose provides a comprehensive set of built-in matchers and assertions, there are times when you need to verify conditions that are unique to your application’s logic. Custom matchers and assertions help you:

  • Simplify complex test conditions.
  • Improve the readability and maintainability of your tests.
  • Create reusable testing components.

Creating Custom Matchers

Custom matchers allow you to define specific conditions that a node must meet. Let’s create a simple custom matcher to verify that a Text composable contains a substring.

Example: Custom Matcher for Substring:

  • Define the Matcher:
fun hasSubstring(substring: String): SemanticsMatcher {
return SemanticsMatcher("${substring} is a substring") { semanticsNode ->
val text = semanticsNode.config.getOrNull(SemanticsProperties.Text)?.firstOrNull()?.text
text?.contains(substring) == true
}
}
  • Use the Matcher in a Test:
@Test
fun testHasSubstring() {
composeTestRule.setContent {
Text("Hello, Jetpack Compose!")
}

composeTestRule.onNode(hasSubstring("Jetpack")).assertExists()
}

In this example:

  • We define a custom matcher hasSubstring that checks if a Text composable contains a specified substring.
  • We use this matcher in a test to verify that the text “Hello, Jetpack Compose!” contains the substring “Jetpack”.

Creating Custom Assertions

Custom assertions allow you to encapsulate complex verification logic into a reusable function. Let’s create a custom assertion to verify that a Text composable starts with a specific prefix.

Example: Custom Assertion for Prefix:

  • Define the Assertion:
fun SemanticsNodeInteraction.assertStartsWith(prefix: String): SemanticsNodeInteraction {
return assert(SemanticsMatcher("${prefix} is a prefix") { semanticsNode ->
val text = semanticsNode.config.getOrNull(SemanticsProperties.Text)?.firstOrNull()?.text
text?.startsWith(prefix) == true
})
}
  • Use the Assertion in a Test:
@Test
fun testAssertStartsWith() {
composeTestRule.setContent {
Text("Hello, Jetpack Compose!")
}

composeTestRule.onNodeWithText("Hello, Jetpack Compose!").assertStartsWith("Hello")
}

In this example:

  • We define a custom assertion assertStartsWith that checks if a Text composable starts with a specified prefix.
  • We use this assertion in a test to verify that the text “Hello, Jetpack Compose!” starts with the prefix “Hello”.

Combining Custom Matchers and Assertions

You can combine custom matchers and assertions to create powerful and expressive tests. Let’s create a more complex example where we need to verify that a Text composable both contains a substring and starts with a specific prefix.

Example: Combined Custom Matcher and Assertion

  • Define the Combined Matcher:
fun hasSubstringAndStartsWith(substring: String, prefix: String): SemanticsMatcher {
return SemanticsMatcher("Text contains '$substring' and starts with '$prefix'") { semanticsNode ->
val text = semanticsNode.config.getOrNull(SemanticsProperties.Text)?.firstOrNull()?.text
text?.contains(substring) == true && text?.startsWith(prefix) == true
}
}
  • Use the Combined Matcher in a Custom Assertion:
fun SemanticsNodeInteraction.assertSubstringAndPrefix(substring: String, prefix: String): SemanticsNodeInteraction {
return assert(hasSubstringAndStartsWith(substring, prefix))
}
  • Use the Combined Assertion in a Test:
@Test
fun testAssertSubstringAndPrefix() {
composeTestRule.setContent {
Text("Hello, Jetpack Compose!")
}

composeTestRule.onNodeWithText("Hello, Jetpack Compose!").assertSubstringAndPrefix("Jetpack", "Hello")
}

In this example:

  • We define a combined matcher hasSubstringAndStartsWith that checks if a Text composable contains a substring and starts with a specific prefix.
  • We use this matcher in a custom assertion assertSubstringAndPrefix.
  • We use this combined assertion in a test to verify that the text “Hello, Jetpack Compose!” contains “Jetpack” and starts with “Hello”.

Best Practices for Custom Matchers and Assertions

  1. Keep It Simple: Aim to keep your custom matchers and assertions simple and focused on a single responsibility.
  2. Reusability: Design your matchers and assertions to be reusable across different tests.
  3. Descriptive Names: Use descriptive names for your custom matchers and assertions to make your tests more readable.
  4. Documentation: Document your custom matchers and assertions to provide context for other developers.

Next Steps: Parameterized Tests 🔄

Ready to enhance your Compose testing skills with more advanced techniques? In our next blog, Compose Testing Advanced 03: Parameterized Tests, we explore how to run the same test logic with different input parameters. This guide will provide detailed examples and best practices for creating and using parameterized tests in Compose, making your tests more robust and reducing code duplication.

Read the next blog: Compose Testing Advanced 03: Parameterized Tests 🔄

Follow Us for More Insights 🌟

Stay updated with the latest tips and best practices for QA engineers in Android UI testing by following us on Medium. For more resources and tools to enhance your testing skills, visit our website EspressoLab.ai.

Follow us on Medium: EspressoLab Medium

Thank you for reading! 🚀

--

--

EspressoLab.Ai

Empowering Software Engineers with top-notch digital products and online courses.