From 2ba32b095137fc53462da63d6e30438d36518160 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:19:06 +0000 Subject: [PATCH 1/3] Initial plan From 7797d61f9a9f22bdc8c34f9d6b0a0417cc1b2910 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:20:57 +0000 Subject: [PATCH 2/3] Extend setup pages with Getting Started guides Co-authored-by: bastianeicher <414366+bastianeicher@users.noreply.github.com> --- docs/setup/dotnet.md | 57 +++++++++++++++++-- docs/setup/java.md | 120 +++++++++++++++++++++++++++++++++++++-- docs/setup/typescript.md | 57 ++++++++++++++++++- 3 files changed, 223 insertions(+), 11 deletions(-) diff --git a/docs/setup/dotnet.md b/docs/setup/dotnet.md index f57a497..07a2553 100644 --- a/docs/setup/dotnet.md +++ b/docs/setup/dotnet.md @@ -1,11 +1,60 @@ # .NET -## Dependencies +## Getting started -Add one or more of the following NuGet packages to your project: +### 1. Install the dependency -[TypedRest](https://www.nuget.org/packages/TypedRest/) -The main TypedRest library. +Add the [TypedRest](https://www.nuget.org/packages/TypedRest/) NuGet package to your project: + +```bash +dotnet add package TypedRest +``` + +### 2. Create an entry endpoint + +Create an `EntryEndpoint` pointing at your API: + +```csharp +using TypedRest; + +var client = new EntryEndpoint(new Uri("https://example.com/api/")); +``` + +### 3. Define your client class + +Extend `EntryEndpoint` and expose your API's resources as properties: + +```csharp +class MyClient(Uri uri) : EntryEndpoint(uri) +{ + public CollectionEndpoint Contacts => new(this, relativeUri: "./contacts"); +} +``` + +### 4. Use the client + +Use your client to interact with the API: + +```csharp +var client = new MyClient(new Uri("https://example.com/api/")); + +// Read all contacts +List contacts = await client.Contacts.ReadAllAsync(); + +// Create a new contact +ContactEndpoint newContact = await client.Contacts.CreateAsync(new Contact { Name = "Smith" }); + +// Read a specific contact +Contact contact = await newContact.ReadAsync(); +``` + +### 5. Next steps + +- Read the [Introduction](../introduction.md) for the full conceptual walkthrough +- Explore all available [Endpoints](../endpoints/index.md) types +- Check out the [API documentation](https://dotnet.typedrest.net/) for detailed reference + +## Additional packages [TypedRest.Reactive](https://www.nuget.org/packages/TypedRest.Reactive/) Adds support for streaming with [ReactiveX (Rx)](http://reactivex.io/) to TypedRest. diff --git a/docs/setup/java.md b/docs/setup/java.md index 014f5dd..6feb7e5 100644 --- a/docs/setup/java.md +++ b/docs/setup/java.md @@ -1,11 +1,123 @@ # Java / Kotlin -## Dependencies +## Getting started -Add one or more of the following Maven artifacts to your project: +### 1. Install the dependency -[typedrest](https://central.sonatype.com/artifact/net.typedrest/typedrest) -The main TypedRest library. +Add the [typedrest](https://central.sonatype.com/artifact/net.typedrest/typedrest) Maven artifact to your project. + +=== "Maven" + + ```xml + + net.typedrest + typedrest + VERSION + + ``` + +=== "Gradle" + + ```groovy + implementation 'net.typedrest:typedrest:VERSION' + ``` + +### 2. Create an entry endpoint + +Create an `EntryEndpoint` pointing at your API: + +=== "Java" + + ```java + import net.typedrest.EntryEndpoint; + import java.net.URI; + + EntryEndpoint client = new EntryEndpoint(URI.create("https://example.com/api/")); + ``` + +=== "Kotlin" + + ```kotlin + import net.typedrest.EntryEndpoint + import java.net.URI + + val client = EntryEndpoint(URI.create("https://example.com/api/")) + ``` + +### 3. Define your client class + +Extend `EntryEndpoint` and expose your API's resources as methods or properties: + +=== "Java" + + ```java + import net.typedrest.CollectionEndpoint; + import net.typedrest.CollectionEndpointImpl; + + class MyClient extends EntryEndpoint { + public MyClient(URI uri) { + super(uri); + } + + public CollectionEndpoint getContacts() { + return new CollectionEndpointImpl<>(this, "./contacts", Contact.class); + } + } + ``` + +=== "Kotlin" + + ```kotlin + import net.typedrest.CollectionEndpoint + import net.typedrest.CollectionEndpointImpl + + class MyClient(uri: URI) : EntryEndpoint(uri) { + val contacts: CollectionEndpoint + get() = CollectionEndpointImpl(this, "./contacts", Contact::class.java) + } + ``` + +### 4. Use the client + +Use your client to interact with the API: + +=== "Java" + + ```java + MyClient client = new MyClient(URI.create("https://example.com/api/")); + + // Read all contacts + List contacts = client.getContacts().readAll(); + + // Create a new contact + ElementEndpoint newContact = client.getContacts().create(new Contact("Smith")); + + // Read a specific contact + Contact contact = newContact.read(); + ``` + +=== "Kotlin" + + ```kotlin + val client = MyClient(URI.create("https://example.com/api/")) + + // Read all contacts + val contacts: List = client.contacts.readAll() + + // Create a new contact + val newContact: ElementEndpoint = client.contacts.create(Contact("Smith")) + + // Read a specific contact + val contact: Contact = newContact.read() + ``` + +### 5. Next steps + +- Read the [Introduction](../introduction.md) for the full conceptual walkthrough +- Explore all available [Endpoints](../endpoints/index.md) types +- Check out the [API documentation](https://java.typedrest.net/) for detailed reference + +## Additional packages [typedrest-reactive](https://central.sonatype.com/artifact/net.typedrest/typedrest-reactive) Adds support for streaming with [ReactiveX (Rx)](http://reactivex.io/). diff --git a/docs/setup/typescript.md b/docs/setup/typescript.md index 34f1106..5b72e61 100644 --- a/docs/setup/typescript.md +++ b/docs/setup/typescript.md @@ -1,10 +1,61 @@ # TypeScript -## Dependencies +## Getting started -Add the `typescript` NPM package to your project: +### 1. Install the dependency - npm install typedrest --save +Add the `typedrest` NPM package to your project: + +```bash +npm install typedrest --save +``` + +### 2. Create an entry endpoint + +Create an `EntryEndpoint` pointing at your API: + +```typescript +import {EntryEndpoint} from 'typedrest'; + +const client = new EntryEndpoint(new URL("https://example.com/api/")); +``` + +### 3. Define your client class + +Extend `EntryEndpoint` and expose your API's resources as properties: + +```typescript +import {EntryEndpoint, CollectionEndpoint} from 'typedrest'; + +class MyClient extends EntryEndpoint { + get contacts() { + return new CollectionEndpoint(this, "./contacts"); + } +} +``` + +### 4. Use the client + +Use your client to interact with the API: + +```typescript +const client = new MyClient(new URL("https://example.com/api/")); + +// Read all contacts +const contacts: Contact[] = await client.contacts.readAll(); + +// Create a new contact +const newContact = await client.contacts.create({name: "Smith"}); + +// Read a specific contact +const contact: Contact = await newContact.read(); +``` + +### 5. Next steps + +- Read the [Introduction](../introduction.md) for the full conceptual walkthrough +- Explore all available [Endpoints](../endpoints/index.md) types +- Check out the [API documentation](https://typescript.typedrest.net/) for detailed reference ## See also From ae3579d68423f5bf1d334e5e60412cbe855d3c87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:22:31 +0000 Subject: [PATCH 3/3] Add Testing section to documentation Co-authored-by: bastianeicher <414366+bastianeicher@users.noreply.github.com> --- docs/.pages | 1 + docs/index.md | 3 + docs/testing/index.md | 402 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 docs/testing/index.md diff --git a/docs/.pages b/docs/.pages index a999859..7c161da 100644 --- a/docs/.pages +++ b/docs/.pages @@ -2,6 +2,7 @@ nav: - index.md - introduction.md - setup + - testing - endpoints - serializers - link-handling diff --git a/docs/index.md b/docs/index.md index deed72f..802b115 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,6 +112,9 @@ TypedRest helps you build type-safe, fluent-style REST API clients. Common REST [Setup](setup/index.md) : How do I use TypedRest in my projects? +[Testing](testing/index.md) +: How to write unit tests for your TypedRest endpoint classes. + [Endpoints](endpoints/index.md) : Documentation for all endpoint types provided by TypedRest. diff --git a/docs/testing/index.md b/docs/testing/index.md new file mode 100644 index 0000000..a7899b2 --- /dev/null +++ b/docs/testing/index.md @@ -0,0 +1,402 @@ +# Testing + +## Introduction + +When building custom endpoint classes with TypedRest, you'll want to write unit tests to ensure they interact with the HTTP layer correctly. The general testing approach is: + +1. **Mock the HTTP layer** — Use a mock HTTP handler or server +2. **Create an `EntryEndpoint`** — Wire it to the mock HTTP infrastructure +3. **Instantiate your endpoint** — Create it as a child of the entry endpoint +4. **Set up expected responses** — Configure the mock to return appropriate HTTP responses +5. **Call the endpoint** — Invoke the method you're testing +6. **Assert requests and results** — Verify that the correct HTTP request was sent and the response was properly deserialized + +## Test infrastructure + +Each platform provides infrastructure for setting up tests with mocked HTTP: + +=== "C#" + + TypedRest tests use xUnit with `[Fact]` attributes and an `EndpointTestBase` abstract class that provides mock HTTP infrastructure: + + ```csharp + using System.Net.Http; + using RichardSzalay.MockHttp; + using TypedRest; + using Xunit; + + public abstract class EndpointTestBase : IDisposable + { + public const string JsonMime = "application/json"; + protected readonly MockHttpMessageHandler Mock = new(); + protected readonly IEndpoint EntryEndpoint; + + protected EndpointTestBase(MediaTypeFormatter? serializer = null) + { + EntryEndpoint = new MockEntryEndpoint(Mock, serializer); + } + + public void Dispose() => Mock.VerifyNoOutstandingExpectation(); + + private class MockEntryEndpoint(HttpMessageHandler messageHandler, MediaTypeFormatter? serializer) + : EntryEndpoint(new HttpClient(messageHandler), new Uri("http://localhost/"), serializer); + } + ``` + + This uses the [RichardSzalay.MockHttp](https://www.nuget.org/packages/RichardSzalay.MockHttp/) package to mock HTTP requests and [FluentAssertions](https://fluentassertions.com/) for assertions. + +=== "Java" + + TypedRest tests use JUnit with `@Test` annotations and an `AbstractEndpointTest` abstract class that provides mock HTTP infrastructure: + + ```java + import net.typedrest.EntryEndpoint; + import okhttp3.mockwebserver.MockWebServer; + import org.junit.jupiter.api.AfterEach; + + abstract class AbstractEndpointTest { + protected MockWebServer server = new MockWebServer(); + protected EntryEndpoint entryEndpoint = new EntryEndpoint(server.url("/").uri()); + + @AfterEach + void after() throws Exception { + server.close(); + } + } + ``` + + This uses [OkHttp's MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) to mock HTTP responses. + +=== "Kotlin" + + TypedRest tests use Kotlin Test with `@Test` annotations and an `AbstractEndpointTest` abstract class that provides mock HTTP infrastructure: + + ```kotlin + import net.typedrest.EntryEndpoint + import okhttp3.mockwebserver.MockWebServer + import kotlin.test.AfterTest + + abstract class AbstractEndpointTest { + protected var server: MockWebServer = MockWebServer() + protected var entryEndpoint: EntryEndpoint = EntryEndpoint(server.url("/").toUri()) + + @AfterTest + fun after() = server.close() + } + ``` + + This uses [OkHttp's MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) to mock HTTP responses. + +=== "TypeScript" + + TypedRest tests use [Jest](https://jestjs.io/) with `test()` and `expect()`, and [jest-fetch-mock](https://www.npmjs.com/package/jest-fetch-mock) to mock the global `fetch` function: + + ```typescript + import fetchMock from 'jest-fetch-mock'; + import {EntryEndpoint, ElementEndpoint} from 'typedrest'; + + fetchMock.enableMocks(); + + let endpoint: ElementEndpoint; + + beforeEach(() => { + fetchMock.resetMocks(); + endpoint = new ElementEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint'); + }); + ``` + +## Example: Testing ElementEndpoint + +Here's a complete example of testing an `ElementEndpoint`'s `read` method: + +=== "C#" + + ```csharp + using System.Net.Http; + using FluentAssertions; + using RichardSzalay.MockHttp; + using TypedRest; + using Xunit; + + public class ElementEndpointTest : EndpointTestBase + { + private readonly ElementEndpoint _endpoint; + + public ElementEndpointTest() + { + _endpoint = new ElementEndpoint(EntryEndpoint, relativeUri: "./endpoint"); + } + + [Fact] + public async Task TestRead() + { + Mock.Expect(HttpMethod.Get, "http://localhost/endpoint") + .Respond(JsonMime, """{"id":5,"name":"test"}"""); + + var result = await _endpoint.ReadAsync(); + + result.Should().Be(new MockEntity { Id = 5, Name = "test" }); + } + } + ``` + +=== "Java" + + ```java + import net.typedrest.ElementEndpoint; + import net.typedrest.ElementEndpointImpl; + import okhttp3.mockwebserver.MockResponse; + import org.junit.jupiter.api.Test; + + import static net.typedrest.tests.MockWebServerExtensions.*; + import static org.junit.jupiter.api.Assertions.*; + + class ElementEndpointTest extends AbstractEndpointTest { + private ElementEndpoint endpoint; + + @BeforeEach + void setUp() { + endpoint = new ElementEndpointImpl<>(entryEndpoint, "./endpoint", MockEntity.class); + } + + @Test + void testRead() throws Exception { + server.enqueue(new MockResponse().setJsonBody("""{"id":5,"name":"test"}""")); + + MockEntity result = endpoint.read(); + + assertEquals(5, result.id); + assertEquals("test", result.name); + server.assertRequest(HttpMethod.GET).withPath("/endpoint"); + } + } + ``` + +=== "Kotlin" + + ```kotlin + import net.typedrest.ElementEndpoint + import net.typedrest.ElementEndpointImpl + import okhttp3.mockwebserver.MockResponse + import kotlin.test.Test + import kotlin.test.assertEquals + + class ElementEndpointTest : AbstractEndpointTest() { + private lateinit var endpoint: ElementEndpoint + + @BeforeTest + fun setUp() { + endpoint = ElementEndpointImpl(entryEndpoint, "./endpoint", MockEntity::class.java) + } + + @Test + fun testRead() { + server.enqueue(MockResponse().setJsonBody("""{"id":5,"name":"test"}""")) + + val result = endpoint.read() + + assertEquals(5, result.id) + assertEquals("test", result.name) + server.assertRequest(HttpMethod.GET).withPath("/endpoint") + } + } + ``` + +=== "TypeScript" + + ```typescript + import fetchMock from 'jest-fetch-mock'; + import {EntryEndpoint, ElementEndpoint} from 'typedrest'; + + fetchMock.enableMocks(); + + let endpoint: ElementEndpoint; + + beforeEach(() => { + fetchMock.resetMocks(); + endpoint = new ElementEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint'); + }); + + test('read', async () => { + fetchMock.mockOnceIf( + 'http://localhost/endpoint', + JSON.stringify({id: 5, name: 'test'}), + {status: 200} + ); + + const result = await endpoint.read(); + + expect(result).toEqual({id: 5, name: 'test'}); + }); + ``` + +## Example: Testing a custom endpoint + +Here's an example of testing a custom endpoint class with additional functionality: + +=== "C#" + + ```csharp + using System.Net.Http; + using FluentAssertions; + using RichardSzalay.MockHttp; + using TypedRest; + using Xunit; + + // Custom endpoint class + public class ContactEndpoint(IEndpoint referrer, Uri relativeUri) + : ElementEndpoint(referrer, relativeUri) + { + public ElementEndpoint Note => new(this, relativeUri: "./note"); + } + + // Test class + public class ContactEndpointTest : EndpointTestBase + { + private readonly ContactEndpoint _endpoint; + + public ContactEndpointTest() + { + _endpoint = new ContactEndpoint(EntryEndpoint, new Uri("./contact", UriKind.Relative)); + } + + [Fact] + public async Task TestReadNote() + { + Mock.Expect(HttpMethod.Get, "http://localhost/contact/note") + .Respond(JsonMime, """{"content":"Test note"}"""); + + var result = await _endpoint.Note.ReadAsync(); + + result.Should().Be(new Note { Content = "Test note" }); + } + } + ``` + +=== "Java" + + ```java + import net.typedrest.Endpoint; + import net.typedrest.ElementEndpoint; + import net.typedrest.ElementEndpointImpl; + import okhttp3.mockwebserver.MockResponse; + import org.junit.jupiter.api.Test; + + import java.net.URI; + + import static org.junit.jupiter.api.Assertions.*; + + // Custom endpoint class + class ContactEndpoint extends ElementEndpointImpl { + public ContactEndpoint(Endpoint referrer, URI relativeUri) { + super(referrer, relativeUri, Contact.class); + } + + public ElementEndpoint getNote() { + return new ElementEndpointImpl<>(this, "./note", Note.class); + } + } + + // Test class + class ContactEndpointTest extends AbstractEndpointTest { + private ContactEndpoint endpoint; + + @BeforeEach + void setUp() { + endpoint = new ContactEndpoint(entryEndpoint, URI.create("./contact")); + } + + @Test + void testReadNote() throws Exception { + server.enqueue(new MockResponse().setJsonBody("""{"content":"Test note"}""")); + + Note result = endpoint.getNote().read(); + + assertEquals("Test note", result.content); + server.assertRequest(HttpMethod.GET).withPath("/contact/note"); + } + } + ``` + +=== "Kotlin" + + ```kotlin + import net.typedrest.Endpoint + import net.typedrest.ElementEndpoint + import net.typedrest.ElementEndpointImpl + import okhttp3.mockwebserver.MockResponse + import java.net.URI + import kotlin.test.Test + import kotlin.test.assertEquals + + // Custom endpoint class + class ContactEndpoint(referrer: Endpoint, relativeUri: URI) : + ElementEndpointImpl(referrer, relativeUri, Contact::class.java) { + val note: ElementEndpoint + get() = ElementEndpointImpl(this, "./note", Note::class.java) + } + + // Test class + class ContactEndpointTest : AbstractEndpointTest() { + private lateinit var endpoint: ContactEndpoint + + @BeforeTest + fun setUp() { + endpoint = ContactEndpoint(entryEndpoint, URI.create("./contact")) + } + + @Test + fun testReadNote() { + server.enqueue(MockResponse().setJsonBody("""{"content":"Test note"}""")) + + val result = endpoint.note.read() + + assertEquals("Test note", result.content) + server.assertRequest(HttpMethod.GET).withPath("/contact/note") + } + } + ``` + +=== "TypeScript" + + ```typescript + import fetchMock from 'jest-fetch-mock'; + import {EntryEndpoint, ElementEndpoint, Endpoint} from 'typedrest'; + + fetchMock.enableMocks(); + + // Custom endpoint class + class ContactEndpoint extends ElementEndpoint { + get note() { + return new ElementEndpoint(this, './note'); + } + } + + // Test setup + let endpoint: ContactEndpoint; + + beforeEach(() => { + fetchMock.resetMocks(); + endpoint = new ContactEndpoint(new EntryEndpoint('http://localhost/'), 'contact'); + }); + + test('read note', async () => { + fetchMock.mockOnceIf( + 'http://localhost/contact/note', + JSON.stringify({content: 'Test note'}), + {status: 200} + ); + + const result = await endpoint.note.read(); + + expect(result).toEqual({content: 'Test note'}); + }); + ``` + +## More examples + +For more comprehensive examples and advanced testing patterns, check out the test suites in the TypedRest implementations: + +- [TypedRest-DotNet tests](https://github.com/TypedRest/TypedRest-DotNet/tree/master/src/UnitTests) +- [TypedRest-Java tests](https://github.com/TypedRest/TypedRest-Java/tree/main/typedrest/src/test) +- [TypedRest-TypeScript tests](https://github.com/TypedRest/TypedRest-TypeScript/tree/master/test)