Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ val user = some<User>()

## Installation

Some is published as three artifacts:
Some is published as four artifacts:

- `some-core` for Java and Kotlin/JVM projects
- `some-android` for Android projects. It re-exports the core API, so you do not need to add `some-core` separately.
- `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`.
- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit.

```kotlin
dependencies {
Expand All @@ -56,6 +57,10 @@ dependencies {
// Optional: Kotest property testing integration.
// Add this alongside either some-core or some-android.
testImplementation("dev.appoutlet:some-kotest:{version}")

// Optional: Retrofit response integration.
// Retrofit must be declared directly by the consuming project.
testImplementation("dev.appoutlet:some-retrofit:{version}")
}
```

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
dokka(projects.android)
dokka(projects.core)
dokka(projects.kotest)
dokka(projects.retrofit)
}

tasks.named("prepareKotlinBuildScriptModel") {
Expand Down
8 changes: 7 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ icon: lucide/graduation-cap

## Installation

Some is published as three artifacts. Pick one base artifact for your platform. Add `some-kotest` only if you use Kotest property testing.
Some is published as four artifacts. Pick one base artifact for your platform. Add `some-kotest` only if you use Kotest property testing and `some-retrofit` only if you use Retrofit.

!!! info "Upgrading from an older version?"

Expand All @@ -24,6 +24,10 @@ dependencies {
// Optional: Kotest property testing integration.
// Add this alongside either some-core or some-android.
testImplementation("dev.appoutlet:some-kotest:{version}")

// Optional: Retrofit response integration.
// Retrofit must be declared directly by the consuming project.
testImplementation("dev.appoutlet:some-retrofit:{version}")
}
```

Expand Down Expand Up @@ -105,6 +109,8 @@ val users = some<List<User>> {

For more configuration use cases and details, see the [Configuration overview](configuration.md), [Strategies](strategies/index.md), and [Supported Types](supported-types.md).

For Retrofit response fixtures, see the [Retrofit2 Integration](retrofit.md) guide.

## Requirements

- **Kotlin 2.3.0+** — required for `typeOf<T>()` and other type introspection APIs
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ icon: lucide/package-open
![Kotlin Android](https://img.shields.io/badge/android-kotlin?style=for-the-badge&logo=android&label=kotlin&color=%231FA95C)
![Maven Central Version](https://img.shields.io/maven-central/v/dev.appoutlet/some-core?style=for-the-badge)

A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` integration.
A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` and Retrofit integrations.

## The Problem

Expand Down Expand Up @@ -70,11 +70,12 @@ Writing tests means creating test data — lots of it. Constructing data classes

## Modules

Some is published as three user-facing artifacts:
Some is published as four user-facing artifacts:

- `some-core` for Java and Kotlin/JVM projects.
- `some-android` for Android projects. It re-exports the shared core API, so you do not need to add `some-core` separately.
- `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`.
- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit.

Use [Getting Started](getting-started.md) to install Some and learn the shared API.

Expand Down
69 changes: 69 additions & 0 deletions docs/retrofit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
icon: lucide/network
---
# Retrofit2 integration

Use `dev.appoutlet:some-retrofit` to generate Retrofit `Response<T>` fixtures for JVM tests.
The integration supports both successful and error responses and is used alongside a direct Retrofit dependency.

## Installation

```kotlin
dependencies {
testImplementation("com.squareup.retrofit2:retrofit:3.0.0")
testImplementation("dev.appoutlet:some-core:{version}")
testImplementation("dev.appoutlet:some-retrofit:{version}")
}
```

`some-retrofit` compiles against Retrofit but does not provide Retrofit transitively. Declare Retrofit directly in the consuming project.

## Generating successful responses

The Retrofit resolver is discovered automatically when `some-retrofit` is on the classpath:

```kotlin
data class User(val id: Int, val name: String)

val response: Response<User> = some()

check(response.isSuccessful)
check(response.code() == 200)
check(response.body() != null)
```

Use `someResponse<T>()` when response metadata matters:

```kotlin
val response: Response<User> = someResponse(
code = 201,
message = "Created",
headers = Headers.Builder().add("X-Test", "value").build(),
request = Request.Builder().url("https://example.com/users").build(),
)
```

The second overload accepts an existing successful `okhttp3.Response`:

```kotlin
val response: Response<User> = someResponse(rawResponse)
```

Successful response codes must be in the `200..299` range, matching Retrofit's `Response.success` contract.

## Generating error responses

Use `someErrorResponse<T>()` for HTTP error handling tests:

```kotlin
val response: Response<User> = someErrorResponse(
code = 404,
message = "Not Found",
)

check(response.body() == null)
check(response.errorBody() != null)
```

The raw-response overload accepts an existing `okhttp3.Response`. Error response codes must be `400` or higher.
The generated error body is empty; add application-specific error-body handling in the test when needed.
4 changes: 4 additions & 0 deletions docs/supported-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ These supported types are part of the shared API consumed through both `some-cor

For Android-specific support, see [Android](android/index.md).

For Retrofit response support, see [Retrofit2 Integration](retrofit.md).

For types not listed here, register a [custom factory](custom-factories.md) or ship a [custom resolver](custom-resolvers.md).

## Reference
Expand Down Expand Up @@ -47,6 +49,8 @@ For types not listed here, register a [custom factory](custom-factories.md) or s
| `Map<K, V>` | `some<Map<String, Int>>()` | |
| `MutableMap<K, V>` | `some<MutableMap<String, Int>>()` | |
| `Array<T>` | `some<Array<String>>()` | |
| **Retrofit** | | |
| `retrofit2.Response<T>` | `some<Response<User>>()` | Requires `some-retrofit` and Retrofit |
| **Android & Compose** | | |
| Android framework | `some<Uri>()`, `some<Rect>()`, ... | See [Android](android/index.md) |
| Jetpack Compose | `some<Dp>()`, `some<Offset>()`, ... | See [Compose](android/compose.md) |
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ kotlin = "2.4.10"
kotest = "6.2.4"
mavenPublish = "0.37.0"
robolectric = "4.16.1"
retrofit = "3.0.0"

[libraries]
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" }
Expand All @@ -21,6 +22,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
Expand Down
69 changes: 69 additions & 0 deletions retrofit/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.mavenPublish)
}

group = rootProject.group
version = rootProject.version

repositories {
mavenCentral()
}

dependencies {
implementation(projects.core)
compileOnly(libs.retrofit)

testImplementation(libs.retrofit)
testImplementation(libs.kotlin.test)

detektPlugins(libs.detekt.formatting)
}

kotlin {
jvmToolchain(17)
}

tasks.test {
useJUnitPlatform()
}

mavenPublishing {
publishToMavenCentral(automaticRelease = true)
signAllPublications()

coordinates(artifactId = "some-retrofit")

pom {
name.set("Some Retrofit")
description.set("Retrofit response integration for Some, a Kotlin test data generation library.")
inceptionYear.set("2026")
url.set("https://github.com/MessiasLima/Some")

licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
}

developers {
developer {
id.set("MessiasLima")
name.set("Messias Lima")
url.set("https://github.com/MessiasLima")
}
}

scm {
url.set("https://github.com/MessiasLima/Some")
connection.set("scm:git:git://github.com/MessiasLima/Some.git")
developerConnection.set("scm:git:ssh://[email protected]/MessiasLima/Some.git")
}
}
}

detekt { autoCorrect = true }
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.appoutlet.some.retrofit

import dev.appoutlet.some.core.Resolver
import dev.appoutlet.some.core.ResolverProvider
import dev.appoutlet.some.core.StrategyProvider
import dev.appoutlet.some.retrofit.resolver.ResponseResolver
import kotlin.random.Random

/**
* Contributes Retrofit-specific resolvers discovered through [java.util.ServiceLoader].
*/
class RetrofitResolverProvider : ResolverProvider {
override fun createResolvers(
strategyProvider: StrategyProvider,
random: Random,
): List<Resolver> = listOf(ResponseResolver())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dev.appoutlet.some.retrofit

import dev.appoutlet.some.some
import okhttp3.Headers
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Response

/**
* Generates an error Retrofit response with an empty error body.
*
* @param T Expected response body type for the endpoint under test.
* @param code Error HTTP status code.
* @param message HTTP status message.
* @param protocol HTTP protocol used by the raw response.
* @param headers HTTP headers used by the raw response.
* @param request Request associated with the raw response.
* @return An error Retrofit response with a `null` body and empty error body.
* @throws IllegalArgumentException If the generated response is successful.
*/
@JvmOverloads
inline fun <reified T> someErrorResponse(
code: Int = 400,
message: String = "Bad Request",
protocol: Protocol = Protocol.HTTP_1_1,
headers: Headers = Headers.Builder().build(),
request: Request = defaultRequest(),
): Response<T> {
val rawResponse = okhttp3.Response.Builder()
.code(code)
.message(message)
.protocol(protocol)
.headers(headers)
.request(request)
.build()

return someErrorResponse(rawResponse)
}

/**
* Generates an error Retrofit response with an empty error body and supplied raw response metadata.
*
* @param T Expected response body type for the endpoint under test.
* @param rawResponse Non-successful OkHttp response to wrap.
* @return An error Retrofit response with a `null` body and empty error body.
* @throws IllegalArgumentException If [rawResponse] is successful.
*/
inline fun <reified T> someErrorResponse(rawResponse: okhttp3.Response): Response<T> {
require(rawResponse.isSuccessful.not()) { "rawResponse must be an error response" }
return Response.error(some<T>().toString().toResponseBody(), rawResponse)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.appoutlet.some.retrofit

import dev.appoutlet.some.some
import okhttp3.Headers
import okhttp3.Protocol
import okhttp3.Request
import retrofit2.Response

private const val DEFAULT_URL = "http://localhost/"

/**
* Generates a successful Retrofit response containing a generated [T] body.
*
* @param T Type of the generated response body.
* @param code Successful HTTP status code.
* @param message HTTP status message.
* @param protocol HTTP protocol used by the raw response.
* @param headers HTTP headers used by the raw response.
* @param request Request associated with the raw response.
* @return A successful Retrofit response containing a generated body.
* @throws IllegalArgumentException If [code] is outside the successful HTTP status range.
*/
@JvmOverloads
inline fun <reified T> someResponse(
code: Int = 200,
message: String = "OK",
protocol: Protocol = Protocol.HTTP_1_1,
headers: Headers = Headers.Builder().build(),
request: Request = defaultRequest(),
): Response<T> {
val rawResponse = okhttp3.Response.Builder()
.code(code)
.message(message)
.protocol(protocol)
.headers(headers)
.request(request)
.build()

return Response.success(some<T>(), rawResponse)
}

/**
* Generates a successful Retrofit response containing a generated [T] body and the supplied raw response metadata.
*
* @param T Type of the generated response body.
* @param rawResponse Successful OkHttp response to wrap.
* @return A successful Retrofit response containing a generated body.
* @throws IllegalArgumentException If [rawResponse] is not successful.
*/
inline fun <reified T> someResponse(rawResponse: okhttp3.Response): Response<T> =
Response.success(some<T>(), rawResponse)

@PublishedApi
internal fun defaultRequest(): Request = Request.Builder().url(DEFAULT_URL).build()
Loading