-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from thumbtack/README
Add README and CONTRIBUTING
- Loading branch information
Showing
2 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Contributing to Kotlin Testing Tools | ||
|
||
Contributions to Thumbtack's Kotlin Testing Tools are welcomed. These contributions can | ||
range from small bug reports to requesting new features. | ||
|
||
Here are a few ways to get started: | ||
|
||
## File a bug or request a feature | ||
|
||
Providing feedback is the easiest way to contribute. You can do this by | ||
[creating an issue on GitHub](https://github.com/thumbtack/kotlin-testing-tools/issues). | ||
|
||
## Contribute code to Kotlin Testing Tools | ||
|
||
There are two ways to contribute code to Kotlin Testing Tools: | ||
|
||
1. **Tackle open GitHub issues:** Issues labeled as “[good first issue](https://github.com/thumbtack/kotlin-testing-tools/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)” or “[help wanted](https://github.com/thumbtack/kotlin-testing-tools/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)” are perfect for contributors that want to tackle small tasks. | ||
2. **Propose an improvement to an existing function or suggest a new function:** Please [create a GitHub issue](https://github.com/thumbtack/kotlin-testing-tools/issues) to propose an enhancement to the repo. Thumbtack will then review the request and will respond on the issue if we decide to move forward with it. | ||
|
||
### Submitting a pull request | ||
|
||
You can create a pull request using the standard `gh pr create` command. Here are a few things to keep in mind when | ||
creating a pull request: | ||
|
||
- **Tests:** Our suite of tests will run automatically on the creation of a pr but you can also run them on your local | ||
branch by running `./gradlew check`. | ||
|
||
- **Creating a local maven JAR:** If you want to test your changes locally before creating a PR, you can publish your | ||
JAR locally by running `./gradlew publishToMavenLocal`. Make sure to add the maven local repo to the application that | ||
is importing Kotlin Testing Tools so it is fetched from there and not JitPack. | ||
|
||
If you're having issues, try changing the version number to one that isn't currently used by JitPack and importing that | ||
version locally. | ||
|
||
## Releasing a new version of Kotlin Testing Tools | ||
|
||
This will be done by a member of Thumbtack Engineering when code has been merged and is ready for release. | ||
|
||
1. From your local repo, pull the latest `main` branch. | ||
2. Run `scripts/release.sh x.y.z`. This will create a new release tag and bump the version number in Gradle. | ||
3. **Create a new release in GitHub:** On the [Releases](https://github.com/thumbtack/kotlin-testing-tools/releases) page | ||
for the repo, click "Draft a new release". Set "Tag version" to the name of the tag you created in step 3 | ||
(e.g., `1.2.3`). Set "Release title" to the same value as the tag version. In the description field, give an overview of the changes going into this release. When all fields have been filled out, click "Publish release." | ||
|
||
--- | ||
|
||
As always, [create an issue](https://github.com/thumbtack/thumbprint-android/issues) if you have questions or feedback. Thank you! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# Kotlin Testing Tools | ||
|
||
This is a collection of utility functions that can be used to help write automation tests for Kotlin code | ||
(e. g. unit tests). Currently this repo contains the `generateTestObject()` function which allows you | ||
to create a fake object for a data class with real, stable values for all fields with just one line of code. | ||
|
||
## How To Use | ||
|
||
Kotlin Testing Tools is published using JitPack. [Follow the instructions here to add it to your project.](https://jitpack.io/#thumbtack/kotlin-testing-tools) | ||
|
||
Then to use `generateTestObject()` simply call: | ||
|
||
```kotlin | ||
val myTestObject = MyTestClass::class.generateTestObject() | ||
``` | ||
|
||
At the moment, only Kotlin/JVM is supported, but suport for other languages in KMP is forthcoming. | ||
|
||
## Documentation | ||
|
||
### Purpose | ||
|
||
The `generateTestObject()` function creates an object for a Kotlin class, without the caller needing to specify each | ||
field’s value. Rather, each field’s value is generated with a unique but consistent value (as opposed to just some | ||
default value like empty string or null). Furthermore, it traverses embedded classes and generates values for their | ||
fields, and so on down the tree, and also creates values for collection objects (lists, maps, etc.). It is particularly | ||
useful for data objects that have a lot of fields, e. g. objects returned in network responses like GraphQL queries. | ||
It avoids the need for developers to have to specify each field for such large objects, thereby saving developer time, | ||
boilerplate, lines of code and test development time. | ||
|
||
### What It Does | ||
|
||
The signature for `generateTestObject` is: | ||
|
||
```kotlin | ||
fun <T : Any> KClass<T>.generateTestObject( | ||
prefix: String = "", | ||
overrides: Map<Regex, Any?>? = null, | ||
referenceDate: Date? = null, | ||
useNullForNullableFields: Boolean = false, | ||
): T | ||
``` | ||
|
||
If you call it with all its defaults: | ||
|
||
```kotlin | ||
val myTestObject = MyTestClass::class.generateTestObject() | ||
``` | ||
|
||
You'll get back an object will all fields filled in with non-empty values: | ||
* Strings are given values of the form `<fieldName>Value`. | ||
* Numbers are set to zero. | ||
* Booleans are set to `false`. | ||
* String collections (lists, sets, arrays): each value is appended their index position. | ||
* Maps are set to the values [(`<fieldName><index>Key`, `<fieldName><index>Value`), ...] | ||
* Nested values are of the form: `<parentFieldName>.<childValue>`. | ||
* Collections of aggregate types have values of the form: `<parentFieldName><index>.<child>Value`. | ||
* Dates and Instants are set to UNIX epoch time unless overridden by [referenceDate] | ||
* Characters are set to 'a' | ||
|
||
You can also choose to have specific fields be of a single value, by passing in a map | ||
where the key is the regex of the field(s) to set, and the value is the value to assign. | ||
Fields are referenced in "dot" notation like `<parent>.<field>`. So if you have the following: | ||
|
||
```kotlin | ||
data class OuterTestClass( | ||
val inner: InnerTestClass, | ||
val one: String | ||
) | ||
data class InnerTestClass( | ||
val one: String | ||
) | ||
``` | ||
|
||
You can pass in an override map of `("inner.one".toRegex() to "MyValue")` to set `inner.one` | ||
to `"MyValue"`. You can set *all* fields called `one` to the same value by specifying | ||
`(".*\.?one".toRegex() to "MyValue")` | ||
|
||
There is also the flag [useNullForNullableFields] that you may enable to enforce all nullable | ||
fields to be populated with null. | ||
|
||
### Philosophy | ||
|
||
When writing tests, you often need to pass in data objects to the method-under-test. Take, for instance, a user | ||
interface test. You want to ensure that all fields in that user interface are rendered correctly. You may have a method | ||
like the following: | ||
|
||
```kotlin | ||
@Compose | ||
fun MyScreen(model: MyModel) { | ||
Column { | ||
Text(model.firstField) | ||
Image(drawableUrl: imageField) | ||
. . . | ||
} | ||
} | ||
``` | ||
|
||
You could pass in a mock, but mocking will typically create “empty” values for each field: typically null for reference | ||
types and zero or “falsy” values for primitives. That won’t render much in a UI test. Furthermore, it won’t test | ||
contained objects, for instance if MyModel contained fields that were also aggregate types or collections: | ||
|
||
```kotlin | ||
data class MyModel( | ||
val firstField: String, | ||
val drawableUrl: String, | ||
val subsection: MySubsection, | ||
val listOfCustomers: List<Customer>, | ||
. . . | ||
} | ||
``` | ||
|
||
So why not just call the class constructor? Problem solved, right? | ||
|
||
But what if your data object had 10 or more fields? And then what if each of those fields were aggregate types each with | ||
several fields? It would involve having to write out a huge constructor call, not to mention taking the time to think of | ||
values for each field! | ||
|
||
That was one of the original intentions, and probably the most important feature, of generateTestObject(): to not only | ||
generate a real object as a test fixture, but to save typing and the tedium of creating those objects. | ||
generateTestObject() will create consistent values for each field of a data class, including nested classes within it as | ||
well as collection types (arrays, lists, maps, etc.). | ||
|
||
But generateTestObject() has another useful benefit. Every single string field has a unique, but predictable, value | ||
within the object; even nested fields and values in collections all have unique values (for strings). This was primarily | ||
done for the benefit of UI testing. So if you have to verify that 10 fields are rendered correctly on the screen, instead | ||
of: | ||
|
||
```kotlin | ||
forViewWithId(viewId1).assertTextValueIs(myModel.field1) | ||
forViewWithId(viewId1).forChildViewWithId(viewId2).assertTextValueIs(myModel.field2) | ||
forViewWithId(viewId1).forChildViewWithId(viewId2).forChildViewWithId(viewId3).assertTextValueIs(myModel.field3) | ||
forViewWithId(viewId4).assertTextValueIs(myModel.field4) | ||
. . . | ||
forViewWithId(viewId10).forChildViewWithid(viewId11).assertTextValueIs(myModel.field10) | ||
``` | ||
|
||
(which can actually be even more complicated than that if you have to traverse hierarchies of views) you can do | ||
something like this: | ||
|
||
```kotlin | ||
forViewWithId(rootViewId).assertHasDescendantsWithValues( | ||
myModel.field1, myModel.field2, myModel.field3, myModel.field4, myModel.field5, myModel.field6, myModel.field7, myModel.field8, myModel.field9, myModel.field10 | ||
) | ||
``` | ||
|
||
It doesn’t save a lot of typing; rather the real savings is in not needing to find exact views because the uniqueness of | ||
each field’s values ensures it gets rendered. True, it’s not as accurate in testing because it could be that a developer | ||
transposed which UI element was supposed to render a particular field; but it does provide a convenient shorthand to at | ||
least generate a test that provides reasonable confidence. | ||
|
||
And generateTestObject() has other benefits: | ||
* Makes it easy to test transformer functions: those functions that transform one data type into another. Especially for | ||
larger data objects. e. g.: | ||
```kotlin | ||
val originObject = OriginData::class.generateTestObject() | ||
val destObject = DestinationData.from(originObject) | ||
assertThat(destObject.header).isEqualTo(originObject.title) | ||
. . . | ||
``` | ||
* Enables you to create a fake backend for developers and automated integration testing. Instead of the tedium of having | ||
to define each fake response, generateTestObject() can do most of the work and we simply provide overrides for those | ||
fields that are critical for our test situation (e. g. particular error conditions, deeplinks) | ||
* Enables us to quickly write screenshot tests and UI previews (like Compose previews) without the tedium of spelling out | ||
every field, only overriding those we need to. | ||
|
||
## Contributing | ||
|
||
Thumbprint accepts issues and pull requests. Take at look at our [contribution instructions](CONTRIBUTING.md) if you'd like to contribute. | ||
## License | ||
Kotlin Testing Tools is licensed under the terms of the [Apache License 2.0](LICENSE). | ||