This is an elaborate example of the ideas in James Shore's Testing Without Mocks pattern language. It has several real-world features:
- Microservice architecture. User requests are processed by a web server and work is performed by a separate microservice (which is part of the same codebase).
- Structured logs. Logs are written as JSON with arbitrary properties.
- Correlation IDs. All logs related to a single user-side request have the same
correlationId
field, which is passed from the web server to the microservice. - Error handling. Microservice failures are handled gracefully, with appropriate logging and error recovery.
- Timeouts and request cancellation. When the microservice doesn't respond quickly enough, the request is cancelled, and the failure is handled gracefully.
For educational purposes, the code is written "close to the metal" with minimal dependencies.
For JavaScript code, see the 'javascript' branch; for TypeScript code, see the 'typescript' branch.
The program is a ROT-13 encoder. It consists of a web server, which provides the web-based user interface, and a ROT-13 microservice, which performs the ROT-13 encoding.
To start the servers, run:
- Mac/Linux:
./serve_dev.sh [web_server_port] [rot13_server_port]
- Windows:
.\serve_dev.cmd [web_server_port] [rot13_server_port]
Access the web interface through a browser. For example, if you started the server with ./serve_dev.sh 5010 5011
, you would visit http://localhost:5010
in your browser.
You’ll need Node.js installed to run the code.
To run the tests, install the version of Node.js listed in package.json under node
. (If you have a different version of Node, the tests will probably work, but you may experience some unexpected test failures.)
-
To run the build and automated tests, run
./watch.sh quick
(Mac/Linux) or.\watch.cmd quick
(Windows). The build will automatically re-run every time you change a file. -
The build only runs tests for files that have changed. Sometimes it can get confused. Restarting the script is usually enough. To make it start from scratch, run
./clean.sh
(Mac/Linux) or.\clean.cmd
(Windows). -
To run the servers, run
./serve_dev 5010 5011
(Mac/Linux) or.\serve_dev.cmd 5010 5011
(Windows). Then visithttp://localhost:5010
in a browser. The server will automatically restart every time you change a file.
Note: The watch
script plays sounds when it runs. (One sound for success, another for lint failure, and a third for test failure.) If this bothers you, you can delete or rename the files in build/sounds
.
Start the servers using the serve command described above. E.g., ./serve_dev.sh 5010 5011
. This starts two servers: a WWW server on port 5010 and a ROT-13 service on port 5011.
The WWW server serves HTML to the user. Access it from a web browser. For example, http://localhost:5010
. The server will serve a form that allows you to encode text using ROT-13. Enter text into the text field and press the "Transform" button. Behind the scenes, the browser will send a "text" form field to the WWW server, which will send it to the ROT-13 service and serve the result back to the browser.
The ROT-13 service transforms text using ROT-13 encoding. In other words, hello
becomes uryyb
.
The service has one endpoint:
- URL:
/rot13/transform
- Method:
POST
- Headers:
content-type: application/json
x-correlation-id
the correlation ID to use in logs
- Body: JSON object containing one field:
text
the text to transform- E.g.,
{ "text": "hello" }
- Success Response:
- Status: 200 OK
- Headers:
content-type: application/json
- Body: JSON object containing one field:
transformed
the transformed text- E.g.,
{ "transformed": "uryyb" }
- Failure Response
- Status: 4xx (depending on nature of error)
- Headers:
content-type: application/json
- Body: JSON object containing one field:
error
the error- E.g.,
{ "error": "invalid content-type header" }
You can make requests to the service directly using your favorite HTTP client. For example, httpie:
~ % http post :5011/rot13/transform content-type:application/json x-correlation-id:my-id text=hello -v
POST /rot13/transform HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 17
Host: localhost:5011
User-Agent: HTTPie/3.2.1
content-type: application/json
x-correlation-id: my-id
{
"text": "hello"
}
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 23
Date: Sat, 04 Feb 2023 08:33:00 GMT
Keep-Alive: timeout=5
content-type: application/json
{
"transformed": "uryyb"
}
The source code is in the src/
directory. Test files start with an underscore and are in the same directories as production code.
- src/: Source code
- all_servers.js (tests): Parse command-line and start servers.
- serve.js: Program entry point. Just launches all_servers.js.
- _smoke_test.js: End-to-end smoke test for both servers.
- node_modules/: Code shared by both servers (not third-party code)
- http/: HTTP infrastructure wrappers
- generic_router.js (tests) A utility for converting HttpServerRequests to method calls.
- http_client.js (tests): Makes HTTP requests.
- http_request.js (tests): Server-side HTTP request received from the client.
- http_server_response.js (tests): Server-side HTTP response to be sent to the client.
- http_server.js (tests): An HTTP server.
- infrastructure/: Other shared infrastructure wrappers
- util/: Miscellaneous libraries
- assert.js (tests): Assertion library used by tests.
- configurable_responses.js (tests): Utility library for implementing Configurable Responses pattern.
- ensure.js (tests): Runtime assertions for production code. Most notably used for runtime type checking of method signatures.
- output_listener.js (tests): Utility library for implementing Output Tracking pattern.
- test_helper.js: Utility library for implementing integration tests.
- type.js (tests): Runtime type checker.
- http/: HTTP infrastructure wrappers
- rot13_service/: ROT-13 microservice
- rot13_controller.js (tests): Controller for
/rot13/transform
endpoint. - rot13_logic.js (tests): ROT-13 encoder.
- rot13_router.js (tests): Entry point into ROT-13 microservice.
- rot13_view.js (tests): Renderer for ROT-13 microservice's responses.
- rot13_controller.js (tests): Controller for
- www/: Front-end website
- www_config.js: Configuration used by all front-end website routes.
- www_router.js (tests): Entry point into front-end website.
- www_view.js (tests): Generic renderer for front-end website’s responses.
- home_page/: Front-end '/' endpoint
- home_page_controller.js (tests): Controller for
/
endpoint. - home_page_view.js (tests): Renderer for
/
responses.
- home_page_controller.js (tests): Controller for
- infrastructure/: Front-end-specific infrastructure wrappers
- rot13_client.js (tests): Client for ROT-13 microservice.
- uuid_generator.js (tests): Create random unique identifiers (UUIDs).
Third-party modules are in the top-level node_modules/
directory (not to be confused with src/node_modules
). The following modules are used by the production code:
@sinonjs/fake-timers
: Used to make Clock Nullable.uuid
: Wrapped by UuidGenerator, which is used to create correlation IDs.
The remaining modules are used by the build and tests:
chai
: Assertion library used by tests.eslint
: Static code analyzer (linter) used by build.gaze
: File system watcher used by build to detect when files change.glob
: File system analyzer used by build to convert globs (such assrc/**/_*_test.js
) to filenames.minimist
: Command-line parser used to parse build's command-line.mocha
: Test runner used by build and tests.shelljs
: Unix command emulator used to simplify aspects of the build.sound-play
: Sound player used by thewatch
script to play sounds when the build completes.
All other files are related to the automated build.
The purpose of this repository is to demonstrate the Testing Without Mocks patterns. Here are each of the patterns in the article and how they're used in this code:
All tests except _smoke_test.js are “narrow tests,” which means they’re focused on a specific class, module, or concept. Most of them are narrow unit tests, but the infrastructure wrappers have narrow integration tests.
All tests are “state-based tests,” which means they make assertions about the return values or state of the unit under test, rather than making assertions about which methods it calls.
All tests are “sociable tests,” which means the code under test isn’t isolated from the rest of the application.
There is one smoke test: the aptly-named _smoke_test.js. It’s a broad integration test that tests the code from end to end. It starts both servers and simulates an HTTP request, then checks the HTML that’s returned.
None of the classes do any significant work in their constructors. The servers have a separate startAsync()
method that is used to start the server after it’s been instantiated.
Every class can be instantiated without providing any parameters. It doesn't make sense for WwwConfig to be instantiated without parameters, so it provides a createTestInstance()
method for use by tests. It has optional defaults.
Almost every test has helper methods that are used to simplify the tests and shield them from changes.
The following tests use Collaborator-Based Isolation to prevent changes in dependencies’ behavior from breaking the tests:
The code is infrastructure-heavy, with almost no logic, so the A-Frame Architecture pattern doesn’t apply to most of the code. However, the ROT-13 service has a small A-Frame Architecture:
- The Application/UI layer is represented by Rot13Router and Rot13Controller.
- The Logic layer is represented by Rot13Logic and Rot13View.
- The Infrastructure layer is represented by HttpServer, HttpServerRequest, and HttpServerResponse.
- There is no Values layer.
Rot13Controller.postAsync() is a Logic Sandwich. It reads data from the HttpServerRequest, calls Rot13Logic, renders it with Rot13View, and then writes data by returning a HttpServerResponse (which is then served by HttpServer).
The WwwRouter and Rot13Router routers are traffic cops. They receive events from the HttpServer via their routeAsync()
methods, then turn around and call the appropriate methods on HomePageController and Rot13Controller. However, because the pattern is spread across multiple classes, it's not very clear in the code.
The code was built evolutionarily, but there's no way to easily see it.
The one Logic layer function, Rot13Logic.transform(), is a pure function. Other classes expose their state as needed to make testing easy.
This program doesn’t use any third-party logic libraries.
There are many infrastructure wrappers. HttpClient, HttpServer, HttpServerRequest, Clock, CommandLine, and UuidGenerator are all low-level infrastructure wrappers. Log and Rot13Client are high-level infrastructure wrappers. (Log uses CommandLine; Rot13Client uses HttpClient.)
The low-level infrastructure wrappers (mentioned above) all have narrow integration tests.
HomePageController and Rot13Client collectively implement Paranoid Telemetry. Rot13Client checks the ROT-13 microservice response for any unexpected behavior, and throws an exception if it finds any. HomePageController handles exceptions thrown by Rot13Client and additionally handles slow responses.
Most classes are nullable.
The low-level infrastructure wrappers (listed above) all have embedded stubs.
The code is written in JavaScript, so thin wrappers aren't needed.
Most of the infrastructure wrappers (listed above) have configurable responses. UuidGenerator and HttpClient in particular support multiple different responses.
Several classes support output tracking: HttpClient, CommandLine, Log, GenericRouter, and Rot13Client.
HttpServer allows callers to simulate HTTP requests.
Log, HomePageController, and Rot13Client all use nullable dependencies to implement their code and tests. Of the production implementations, Rot13Client is the most interesting, because it has configurable responses.
The code was a green-field project, so the legacy code patterns weren't needed.
Copyright (c) 2020-2023 Titanium I.T. LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.