- Introduction
- Scope
- Book theses overview
- Implementation
- Repository building
- Install
- Compare implementations
This code* has been ported from a copyrighted C# version, included in the Unit Testing Principles, Practices, and Patterns book, published by Manning. The author, Vladimir Khorikov, has explicitly allowed such use here. Otherwise stated, all information and quotes in this file comes from the book. I'm not linked in any way with the author; I would like to experiment its proposals and share them with you.
*: except PostgreSQL PL/SQL version, that I wrote by myself. Referred to as pg-pl-sql in codebase, it'a procedural (imperative) language, executed by the database. Basically, it allows mixing "functional" SQL statements together using basic imperative structures (control flow, variables, array). The aim of using such an unusual programming language is to show cost/benefits of such a compact implementation.
Regarding hexagonal architecture, there is a plenty of folders naming convention (see the update note in this blog post):
- domain: port / adapter
- user-side / application
- server-side / infrastructure
Characterization testing complies with to Michael Feathers definition
Only entreprise applications are in the scope.
An entreprise application is an application that aims at automating or assisting an organization's inner processes. It can take many forms, but usually the characteristics of an entreprise software are:
- high business logic complexity;
- long project lifespan;
- moderate amounts of data;
- low or moderate performance requirements.
Only back-office API is in the scope (GUI are off-scope).
Automated testing aims at making change cheaper, by making sure:
- the new behaviour is what you expected
- other existing behaviours have not been affected
Automated testing is implemented by code, which should be payed for several times:
- implementation time;
- maintenance time.
Therefore, test can make change costlier.
Several ways to design tests:
- mockist school
- mock all dependencies (eg. most collaborators)
- this cause test code bloat and test brittleness: such test raises false positive while refactoring
- classical school
- mock only shared dependencies (eg. DB)
- this cause less, but still some test brittleness
- output-based test
- mock only unmanaged shared dependencies (eg. external API)
- brittleness is minimum
- output-based test can be achieved
- by restricting side effect to dedicated areas in production code
- such containment is provided by indirection, implemented by application architecture pattern: hexagonal, functional
Output-based test look like the best bet.
Transitioning a codebase to output-based testing is a 2 steps refactoring process:
- refactor production code (thus breaking existing brittle test) to achieve containment
- refactor test code
Coding is a tradeoff between code performance and change cost
Indirection | Code change | Code performance | Test change |
---|---|---|---|
None | costlier | higher | costlier |
Some | cheaper | lower | cheaper |
Too many | costlier | lower | costlier |
Indirection refers to can be implemented by design patterns, layered architecture.
Code change (maintenance) cost can be broken down in time:
- to understand existing code
- to make appropriate change
- fix unintended side effects (regression)
Test change (maintenance) cost can be broken down in time:
- to understand existing test
- to make appropriate change to support a code feature change
- to make appropriate change to support a code refactoring change (false positive)
1: Indirection samples are design patterns, layered architecture
Test types:
- unit test
- scope :
- domain only
- all paths
- isolation :
- use real collaborators
- no mock
- scope :
- integration test:
- scope :
- happy path, exercice all components in the least possible scenarios
- all other paths untested in unit test
- isolation:
- use real collaborators in
- application, domain
- infrastructure : managed dependencies only (eg. DB)
- mock
- infrastructure: unmanaged dependency only (here, external message bus/API)
- using handwritten mock
- use real collaborators in
- scope :
Codebase comes in several flavors, to widen understanding:
- procedural (no OOP)
- pg-pl-sql
- Javascript
- object-oriented
- hexagonal architecture
It will follow these steps:
- port procedural C# codebase from the book to a procedural JS
- write characterization test
- write pg-pl-sql port, using characterization test
- port OOP hexagonal C# codebase from the book to JS OOP hexagonal
- write test for OOP hexagonal
- with best practice from book
- with anti-patterns (eg. unit-testing everything, using mocks)
- compare costs and benefits of each solution
You'll need:
- node and npm (I used nvm)
- a running postgresql instance:
- I used Docker
- to make optional http call, you'll need http extension)
- get the source:
- with git: clone the repo :
git clone git@github.com:GradedJestRisk/refactoring-test.git && cd refactoring-test
- without git:
- or download the source code manually, extract and cd
curl -LJO https://github.com/GradedJestRisk/refactoring-test/archive/master.zip unzip refactoring-test-master.zip cd refactoring-test-master
- with git: clone the repo :
- install dependencies
npm install
- setup your database connection in knexfile.js
connection: {
database: '<DATABASE_NAME>',
port: <PORT>,
user: '<USER',
password: '<PASSWORD>'
},
- create DB structure: run
npm run db:create-schema
- run the test
npm test
To interactively make API calls on OOP Hexagonal JS:
- create sample data with
npm run db:insert-data
- start API with
npm start
- check it's up
- make a health check call
curl --request GET 'http://localhost:3000/health_check'
- check the response
{"name":"refactoring-test","version":"1.0.0","description":"javascript port of https://www.manning.com/books/unit-testing C# refactor kata"}%
- check the log
127.0.0.1: GET /health_check --> 200
- make a health check call
- make an actual call
- get user:
curl --request GET 'http://localhost:3000/users/0'
- change its email:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"id":"0","email":"foo@bar.com"}' \
localhost:3000/users/0/email
- get user:
To run characterization tests interactively in your IDE on an implementation:
- alter the following line in change-user-email.test.js
} else {
// used for interactive
sutPath = sutPathProceduralJS;
}
To see all SQL queries issued by JS:
- enable debug mode by uncommenting the following line in knexfile.js
// debug: true
Units:
- time: milliseconds
- size: characters
Implementation | Code size |
Code execution time |
Unit test size |
Unit test execution time |
Integration test size |
Integration test execution time |
Char. test execution time |
End-to-end test size |
End-to-end test execution time |
---|---|---|---|---|---|---|---|---|---|
Procedural DB | 4 250 | 1 (3 from node) | N/A | N/A | N/A | N/A | 1 000 | N/A | N/A |
Procedural JS | 2 750 | 220 | N/A | N/A | N/A | N/A | 2 000 | N/A | N/A |
OOP Hexagonal JS | 5 590 | 220 | 2 500 | 370 | 4 258 | 570 | 2 000 | 3 496 | 2 101 |
Helper | Code |
---|---|
Char test | 9 000 |
Char test helper | 1 000 |
Run npm run benchmark
To get procedural pl/sql execution time out of node, execute function SELECT get_execution_time_micro();