test-accessors
An annotation processor that generates code for your tests to be able to access and modify private/final fields so you don't have to use anti-patterns such as @VisibleForTesting!
Usage
Add the dependencies to your project:
repositories {
google()
jcenter()
}
dependencies {
// 1. Add the annotation
implementation "com.github.stoyicker.test-accessors:annotations:<version>"
// 2. Add the processor you want
// If on Java
annotationProcessor "com.github.stoyicker.test-accessors:processor-java:<version>"
// If on Kotlin (or mixed Java/Kotlin)
kapt "com.github.stoyicker.test-accessors:processor-kotlin:<version>"
}
Annotate your field:
public final class MyClass {
@RequiresAccessor(requires = {RequiresAccessor.AccessorType.TYPE_GETTER, RequiresAccessor.AccessorType.TYPE_SETTER})
private final String myField = "hola hola";
}
Once annotation processing runs, there will be a class in the generated directory of your source set with two methods like this:
public final class MyClassTestAccessors {
public static <T> T myField(final MyClass receiver);
public static <T> void myField(final MyClass receiver, final T newValue);
}
The processor-kotlin artifact achieves the same goal through Kotlin extension methods for the class that owns the annotated fields for more idiomatic accessor usage.
internal object MyClassTestAccessors {
fun <T> MyClass.myField(): T
fun <T> MyClass.myField(newValue: T)
}
As you can see, things work perfectly fine even with finals/vals. Moreover, it also works with static variables!
class MyClass {
@RequiresAccessor(requires = {RequiresAccessor.AccessorType.TYPE_GETTER, RequiresAccessor.AccessorType.TYPE_SETTER})
private static String myStaticField = "static hola hola";
}
will generate an implementation under the following API in the current source set:
public final class MyClassTestAccessors {
public static <T> T myStaticField();
public static <T> void myStaticField(final T newValue);
}
Or, in Kotlin:
internal object MyClassTestAccessors {
fun <T> myStaticField(): T
fun <T> myStaticField(newValue: T)
}
These methods will have Kotlin compatibility annotations to ensure that the way to access them from Java stays similar to the API generated by the processor-java artifact, which makes processor-java and processor-kotlin 100% interchangeable (although you will need Kotlin for processor-kotlin since, well, it generates Kotlin code). Accessors for fields in objects and companion objects will be similar to the ones generated by the processor-java artifact for static fields. Accessors for top-level fields will be similar but generated in a file with "Kt" appended at the end of the target file name, following how Kotlin translates top-level fields and functions to Java bytecode.
The different sample projects within the repo showcase how to use all of the possibilities that both processors offer, so feel encouraged to check them out if you find yourself lost!
Options
Annotation level
The annotation has some parameters you can use to alter its behavior:
- name -> Allows you to change the name of the methods that will be generated for the field you are annotating. If unspecified, the name of the field will be used.
- requires -> Allows you to specify which type of accessor you want (use AccessorType.TYPE_GETTER for getter and/ or AccessorType.TYPE_SETTER for setter) for your annotated field. If unspecified, only a setter will be generated.
- androidXRestrictTo -> Allows you to declare an instance of androidx.annotation.RestrictTo that will be added to the method(s) generated due to this annotation. If unspecified or the scope array that describes the restrictions is empty, no androidX RestrictTo annotation will be added to methods generated due to this annotation occurrence (unless overriden by testaccessors.defaultAndroidXRestrictTo, see below).
- supportRestrictTo -> Allows you to declare an instance of android.support.annotation.RestrictTo that will be added to the method(s) generated due to this annotation. If unspecified or the scope array that describes the restrictions is empty, no support RestrictTo annotation will be added to methods generated due to this annotation occurrence (unless overriden by testaccessors.defaultSupportRestrictTo, see below).
Processor level
- testaccessors.requiredClasses -> Allows you to specify a list of comma-separated class names that will be 'pinged' every time a generated method runs, triggering an exception if none of them are found. This allows you to ensure that generated methods are not used where they should not (such as outside of tests) by passing in classes that are specific to test artifacts. If unspecified, it becomes a list of junit.runner.BaseTestRunner (for JUnit 4), org.junit.jupiter.api.Test (for JUnit 5) and org.testng.TestNG (for TestNG).
- testaccessors.defaultAndroidXRestrictTo -> Allows you to specify a default androidx.annotation.RestrictTo scope that will cause all occurrences of RequiresAccessors to change their default value for androidXRestrictTo to an instance of RestrictTo with that scope. Must be a comma-separated String formed by one or more of "LIBRARY", "LIBRARY_GROUP", "GROUP_ID", "TESTS" and "SUBCLASSES".
- testaccessors.defaultSupportRestrictTo -> Allows you to specify a default android.support.annotation.RestrictTo scope that will cause all occurrences of RequiresAccessors to change their default value for supportRestrictTo to an instance of RestrictTo with that scope. Must be a comma-separated String formed by one or more of "LIBRARY", "LIBRARY_GROUP", "GROUP_ID", "TESTS" and "SUBCLASSES".
How do I pass arguments to the annotation processor?
Frameworkless Java:
compileJava {
options.compilerArgs.addAll(['-Atestaccessors.requiredClasses=yourFirstClass,yourSecondClass'])
}
Android with Java:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = ['testaccessors.requiredClasses': 'yourFirstClass,yourSecondClass']
}
}
}
}
Kotlin:
kapt {
arguments {
arg('testaccessors.requiredClasses', 'yourFirstClass,yourSecondClass')
}
}
Drawbacks?
If you don't use the generated methods outside of tests, a simple shrinking ProGuard configuration such as this one will make sure that your classpath does not get affected at all. Additionally, all accessors are generated in the last round of annotation processing, which means they will not be considered by other annotation processors and therefore won't slow down their executions.
Known issues
- Accessors for Kotlin member properties are not generated when using processor-kotlin: They are, but the IDE cannot resolve the import due to this bug in IntelliJ IDEA. Star and upvote the issue to help getting it fixed quicker! In the meantime, you will need to write the import manually (see the 'kotlin' flavor tests for AndroidApplication in sample-android) or use processor-java instead of processor-kotlin.