question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Provide support for Kotlin extension methods on Subjects

See original GitHub issue

The title might be a little off. We recently converted the Point of Sale Android codebase at Square from AssertJ to Truth and used Kotlin for our custom Subjects. I try to walk you through the issues I ran into, maybe it is beneficial for your future API considerations.

Kotlin supports extension function, what allows “extending” existing Subjects easily and in a fluent way. My goal was to extend the StringSubject with checks that compresses multiple whitespaces into one similar ignoreCase().

assertThat("hello   world").compressWhitespaces().isEqualTo("hello world")

From Java’s point of view compressWhitespaces() is like a static method that has StringSubject as argument.

The first problem I ran into is that actual() is protected final in Subject. There wasn’t a way for me to get the actual value from the StringSubject that I’m “extending”. (My solution is to use a reflective call for now)

Similar to StringSubject.CaseInsensitiveStringComparison I planned to implement the checks in a class, that doesn’t extend Subject. This way I can add methods like isEqualTo() when I actually implement them. There were multiple problems with this approach:

  1. CaseInsensitiveStringComparison is an inner class, it gets access to actual() and other methods through StringSubject. My class doesn’t have access to these methods. My biggest problem was that I couldn’t construct failure messages easily. I have the impression that Subject has too many concerns: It provides the API for checks, but also gives you access to methods to create error messages. It would be helpful, if both things were separated.
  2. So my class had to extend Subject. I could hide the implementation behind an interface, but this wouldn’t allow providing a factory method that creates the Subject.Factory, what is necessary for assertAbout(..) and quite useful in Java.
assertAbout(compressWhitespaces()).that("string   string").isEqualTo("string string")
  1. I had to expose that my class extends Subject, meaning every caller sees all the methods, that I might not have implemented. In fact, my class implements isEqualTo() right now, but not isNotEqualTo(). You could treat it like a not finished implementation for the API and I could throw an exception / error when calling this method, but from an API point of view the caller doesn’t know that. They shouldn’t see the method at all. This leads to false positives at the moment.
assertAbout(compressWhitespaces()).that("string   string").isNotEqualTo("string string") // doesn't throw at the moment

To summarize those are the things I wish would be available or different.

  • Get access to the actual() value from outside.
  • Make creating error messages possible without extending Subject.
  • Split the default check methods into separate classes or interfaces and provide default implementations in a composable way.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:7
  • Comments:10 (8 by maintainers)

github_iconTop GitHub Comments

1reaction
vRallevcommented, Apr 9, 2019

I need to transform both values, so the API wouldn’t be nice if compressWhitespaces() is on the String. My goal was also that people type assertThat(string). and get a list of all options with auto-complete to improve discoverability.

0reactions
efemoneycommented, Apr 18, 2022

Running into this issue (somewhat). My use case is also very similar:

I need to assert that certain lines of a multi-line string (coming from an existing subject chain) starting with a specific token, end with another token

Something like:

assertThat(gradleBuild()) // 3rd party truth extension
  .output() // multi line sting -> StringSubject
  .lines() // extension I want to add -> 'LinesSubject'
  .startingWith("my-start-token") // Filtered 'LinesSubject'
  .endsWith(expected)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Extensions | Kotlin
Kotlin provides the ability to extend a class or an interface with new functionality without having to inherit from the class or use...
Read more >
Everything you need to know about Kotlin extensions
Use Kotlin extensions to add functions to a class you cannot modify, add new properties to existing classes, and extend companion objects.
Read more >
Extension Functions in Kotlin - Baeldung
A quick guide to using and creating extension methods in Kotlin.
Read more >
Using Kotlin Extension Functions: The Good, the Bad, and the ...
Extension functions allow you to place almost anything in them (like any other method), but you need to do this very carefully. Because...
Read more >
Work with extensions - Learn Kotlin - OpenClassrooms
However, extensions in Kotlin will not allow you to bypass the encapsulation of the class! You will not be able to access methods...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found