Decorative site banner
Project icon

com.io7m.quixote

  • About
  • Releases
  • Manual
  • Sources
  • License
  • Issues
Maven Central Version Maven Snapshot Code Coverage

quixote


A tiny embedded HTTP server for unit testing.

Features


  • An embedded HTTP server for simulating external services.
  • Conveniently enqueue responses to arbitrary requests.
  • Verify that requests were received as expected.
  • Zero dependencies for the core module (optional modules may have dependencies).
  • Written in pure Java 21.
  • OSGi ready.
  • JPMS ready.
  • ISC license.
  • High-coverage automated test suite.

Motivation


Any code that makes requests to an external service should be tested to ensure that both the requests it makes are correct, and that it behaves correctly when presented with various responses. The quixote package provides a tiny embedded web server that can be configured to return canned responses to specified requests.

Building


$ mvn clean verify

Usage


Create a QWebServer before each test. The server will listen on the specified port.

@BeforeEach public void setup() throws IOException { this.server = QWebServers.createServer(42000); this.http = HttpClient.newHttpClient(); }

Enqueue responses to requests:

this.server.addResponse() .forMethod("GET") .forPath("/xyz") .withContentType("text/plain") .withFixedText("Hello 0.") .withStatus(201) .withHeader("Header-0", "XYZ");

Have code make requests to the server during tests, and then verify that the server received the requests:

final var requests = new LinkedList<>(this.server.requestsReceived()); { final var req = requests.remove(0); assertEquals("GET", req.method()); assertEquals("/xyz", req.path()); }

Remember to clean up the server after each test:

@AfterEach public void tearDown() throws IOException { this.server.close(); }

OCI


quay.io/repository/io7mcom/quixote

The quixote server is also capable of acting as a standalone web server for use in integration tests involving containers. This is primarily useful for situations where an integration test starts multiple containers inside an isolated virtual network, and one or more of the test containers need to speak to a fake web server, and the host running the test suite needs to observe the requests in question. This particular configuration can be difficult to set up in an automated manner, as most virtual networking solutions don't allow for containers to "talk back" to the host machine. Using quixote as a container allows for it to be present inside the virtual network, and for the host to download and parse request logs after the test has completed, sidestepping the need for any containers to know about the host machine.

In other words:

  1. The host creates a virtual network using its container engine of choice.
  2. The host creates several containers inside the virtual network.
  3. The host writes a quixote configuration file in the host directory.
  4. The host creates a quixote container inside the virtual network, mounting the host directory into the container as a volume mount.
  5. The host runs the test. This causes the containers inside the virtual network to make requests to the quixote container.
  6. The host reads the resulting request log from the host directory and verifies that the correct requests were made for the test.

Virtual Network

Given a configuration file conforming to the provided schema, the quixote server can be started using a container engine such as Podman.

$ cat quixote/config.xml <?xml version="1.0" encoding="UTF-8" ?> <Configuration xmlns="urn:com.io7m.quixote:configuration:1" Port="20001" GZIP="true"> <Responses> <Response Method="GET" Path="/" Status="200"> <Headers> <Header Name="Content-Type" Value="application/octet-stream"/> </Headers> <ContentUTF8>Hello world!</ContentUTF8> </Response> </Responses> </Configuration>

$ podman run \ --rm \ --interactive \ --tty \ --volume quixote:/quixote/data:rw \ --publish 20001:20001/tcp \ quay.io/io7mcom/quixote:1.2.0 \ /quixote/data/config.xml \ /quixote/data/output.bin INFO com.io7m.quixote.main.Main: Quixote running at http://localhost:20001/

$ curl http://localhost:20001/ Hello world!

The server appends every received request to a file consisting of an array of request records in a trivial binary format. A request record has the following structure:

RequestRecord { Unsigned64 quixote; Unsigned32 version; Unsigned64 length; Unsigned8 data[length]; }

All integer values are in big-endian byte order.

The quixote field of the RequestRecord structure always has the value 0x515549584F544521 ("QUIXOTE!" in ASCII). The version field of the RequestRecord field currently has the value 0x00000001.

The data field is a sequence of length bytes that represents a serialized java.util.Properties XML document. For example, the curl request above resulted in the following record being appended to /quixote/output.bin:

0000:0000 | 51 55 49 58 4F 54 45 21 00 00 00 01 00 00 00 00 | QUIXOTE!........ 0000:0010 | 00 00 01 BF 3C 3F 78 6D 6C 20 76 65 72 73 69 6F | ...¿<?xml versio 0000:0020 | 6E 3D 22 31 2E 30 22 20 65 6E 63 6F 64 69 6E 67 | n="1.0" encoding 0000:0030 | 3D 22 55 54 46 2D 38 22 3F 3E 0A 3C 21 44 4F 43 | ="UTF-8"?>.<!DOC 0000:0040 | 54 59 50 45 20 70 72 6F 70 65 72 74 69 65 73 20 | TYPE properties 0000:0050 | 53 59 53 54 45 4D 20 22 68 74 74 70 3A 2F 2F 6A | SYSTEM "http://j 0000:0060 | 61 76 61 2E 73 75 6E 2E 63 6F 6D 2F 64 74 64 2F | ava.sun.com/dtd/ 0000:0070 | 70 72 6F 70 65 72 74 69 65 73 2E 64 74 64 22 3E | properties.dtd"> 0000:0080 | 0A 3C 70 72 6F 70 65 72 74 69 65 73 3E 0A 3C 65 | .<properties>.<e 0000:0090 | 6E 74 72 79 20 6B 65 79 3D 22 48 65 61 64 65 72 | ntry key="Header 0000:00A0 | 2E 72 65 6D 6F 74 65 2D 61 64 64 72 22 3E 31 32 | .remote-addr">12 0000:00B0 | 37 2E 30 2E 30 2E 31 3C 2F 65 6E 74 72 79 3E 0A | 7.0.0.1</entry>. 0000:00C0 | 3C 65 6E 74 72 79 20 6B 65 79 3D 22 49 6E 66 6F | <entry key="Info 0000:00D0 | 2E 4D 65 74 68 6F 64 22 3E 47 45 54 3C 2F 65 6E | .Method">GET</en 0000:00E0 | 74 72 79 3E 0A 3C 65 6E 74 72 79 20 6B 65 79 3D | try>.<entry key= 0000:00F0 | 22 48 65 61 64 65 72 2E 68 74 74 70 2D 63 6C 69 | "Header.http-cli 0000:0100 | 65 6E 74 2D 69 70 22 3E 31 32 37 2E 30 2E 30 2E | ent-ip">127.0.0. 0000:0110 | 31 3C 2F 65 6E 74 72 79 3E 0A 3C 65 6E 74 72 79 | 1</entry>.<entry 0000:0120 | 20 6B 65 79 3D 22 49 6E 66 6F 2E 50 61 74 68 22 | key="Info.Path" 0000:0130 | 3E 2F 3C 2F 65 6E 74 72 79 3E 0A 3C 65 6E 74 72 | >/</entry>.<entr 0000:0140 | 79 20 6B 65 79 3D 22 48 65 61 64 65 72 2E 61 63 | y key="Header.ac 0000:0150 | 63 65 70 74 22 3E 2A 2F 2A 3C 2F 65 6E 74 72 79 | cept">*/*</entry 0000:0160 | 3E 0A 3C 65 6E 74 72 79 20 6B 65 79 3D 22 48 65 | >.<entry key="He 0000:0170 | 61 64 65 72 2E 75 73 65 72 2D 61 67 65 6E 74 22 | ader.user-agent" 0000:0180 | 3E 63 75 72 6C 2F 38 2E 37 2E 31 3C 2F 65 6E 74 | >curl/8.7.1</ent 0000:0190 | 72 79 3E 0A 3C 65 6E 74 72 79 20 6B 65 79 3D 22 | ry>.<entry key=" 0000:01A0 | 48 65 61 64 65 72 2E 68 6F 73 74 22 3E 31 32 37 | Header.host">127 0000:01B0 | 2E 30 2E 30 2E 31 3A 32 30 30 30 31 3C 2F 65 6E | .0.0.1:20001</en 0000:01C0 | 74 72 79 3E 0A 3C 2F 70 72 6F 70 65 72 74 69 65 | try>.</propertie 0000:01D0 | 73 3E 0A | s>.

Extracting the XML document and formatting it gives:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <entry key="Header.remote-addr">127.0.0.1</entry> <entry key="Info.Method">GET</entry> <entry key="Header.http-client-ip">127.0.0.1</entry> <entry key="Info.Path">/</entry> <entry key="Header.accept">*/*</entry> <entry key="Header.user-agent">curl/8.7.1</entry> <entry key="Header.host">127.0.0.1:20001</entry> </properties>

We can see that the client made a GET request to the / path. The QWebRequestLogging class provides convenient functions to read and write request logs.

Releases & Development Snapshots


Releases


You can subscribe to the atom feed to be notified of project releases.

The most recently released version of the package is 1.3.0.

1.3.0 Release (2024-05-10Z)

  • Update ch.qos.logback:logback-classic:1.5.3 → 1.5.6
  • Update org.slf4j:slf4j-api:2.0.12 → 2.0.13
  • Update com.io7m.blackthorne:com.io7m.blackthorne.core:2.0.0 → 2.0.1
  • Update com.io7m.blackthorne:com.io7m.blackthorne.jxe:2.0.0 → 2.0.1
  • Update com.io7m.jxe:com.io7m.jxe.core:1.0.2 → 1.0.3
  • Update com.io7m.anethum:com.io7m.anethum.api:1.1.0 → 1.1.1
  • Update com.io7m.anethum:com.io7m.anethum.slf4j:1.1.0 → 1.1.1
  • Update com.io7m.jlexing:com.io7m.jlexing.core:3.1.0 → 3.2.0

The compiled artifacts for the release (and all previous releases) are available on Maven Central.

Maven Modules


<dependency> <group>com.io7m.quixote</group> <artifactId>com.io7m.quixote.core</artifactId> <version>1.3.0</version> </dependency><dependency> <group>com.io7m.quixote</group> <artifactId>com.io7m.quixote.main</artifactId> <version>1.3.0</version> </dependency><dependency> <group>com.io7m.quixote</group> <artifactId>com.io7m.quixote.oci</artifactId> <version>1.3.0</version> </dependency><dependency> <group>com.io7m.quixote</group> <artifactId>com.io7m.quixote.tests</artifactId> <version>1.3.0</version> </dependency><dependency> <group>com.io7m.quixote</group> <artifactId>com.io7m.quixote.xml</artifactId> <version>1.3.0</version> </dependency>

Previous Releases


The changelogs for the most recent previous releases are as follows:

1.2.0 Release (2024-04-03Z)

  • Update ch.qos.logback:logback-classic:1.4.14 → 1.5.3
  • Update org.slf4j:slf4j-api:2.0.10 → 2.0.12
  • Update org.junit.jupiter:junit-jupiter-api:5.10.1 → 5.10.2
  • Update org.junit.jupiter:junit-jupiter-engine:5.10.1 → 5.10.2
  • Add OCI container support.

1.1.0 Release (2023-11-03Z)

  • Add an API for controlling the web server address binding.

1.0.0 Release (2023-06-11Z)

  • Initial major release.

Development Snapshots


At the time of writing, the current unstable development version of the package is 1.3.1-SNAPSHOT.

Development snapshots may be available in the Central Portal Snapshots repository. Snapshots are published to this repository every time the project is built by the project's continuous integration system, but snapshots do expire after around ninety days and so may or may not be available depending on when a build of the package was last triggered.

Manual


This project does not have any user manuals or other documentation beyond what might be present on the page above.

Sources


This project uses Git to manage source code.

Repository: https://www.github.com/io7m-com/quixote

$ git clone --recursive https://www.github.com/io7m-com/quixote

Issues


This project uses GitHub Issues to track issues.

License


Copyright © 2024 Mark Raynsford <code@io7m.com> https://www.io7m.com Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Last Updated 2025-08-09T14:58:19Z