A Scala library with great ZIO integration to help you easily write high-level integration/black box tests.
zzspec
helps you grow your confidence in the correctness of software with ZIO (test).
Testing at a high level means freedom of implementation and refactoring, this is something all engineers love. Also, we love functional programming, Scala and ZIO ❤️.
Tests should be the compass to study and prove the correctness of the behaviour of a system.
To get started using zzspec
, a good place to look is the integration tests of zzspec
itself.
See the PostgreSQL spec PostgreSQL spec here.
See the OpenSearch spec OpenSearch spec here.
See the Kafka spec Kafka spec here.
See the MockServer spec MockServer here.
See an example “real world” test which combines MockServer and PostgreSQL here, with prepared DB state and post-run DB checks: docs/examples/real-world-postgre-and-mockserver.scala.
zzspec
considers the following to be essential for good testing:
- allow engineers to focus on the actual test flow at a high level of reasoning, with an expressive language (DSL), abstracting away certain details (always with an escape hatch).
- concurrent testing should work and be the default (with random order of text execution to prove correctness)
- speed of test execution, having ZIO test really helps us by leveraging layers (per suite, per test etc.)
- tests should be first class citizens of your endeavours, and a good way to teach more details about the system to others
- up to date dependencies (e.g. docker container versions, 3rd party code)
- pragmatism as starting point and on a case by case basis, decide what kind of tests do enough coverage of the system and control flow. traditional code coverage metrics are not always applicable.
- contract-driven testing is a great way to ensure data schemas and values remain consistent, when testing with external systems for example.
zzspec
can help and has functionalities for this. - mocks are most of the time not the right tool to test our systems and logic
- no marrying the test structure to code structure. No testing every individual class when not needed.
- ensure we test functionalities, almost never test how the code is written / implemented (this gives flexibility and freedom to refactor).
- attempt to test all “user paths” and possible interactions with the system, good and bad, low load high load, etc.
- write many unit tests and property based tests where it makes sense. Preferably auto-generated test cases, with a set of inputs.
You can install zzspec
just like any other Maven/Nexus package, by adding it as dependency to your project:
libraryDependencies += ("io.github.jjba23" %% "zzspec" % "0.9.3" % Test) // or newer
ZZSpec will also pull some libraries with it, including ZIO, Circe and testcontainers. Artifacts are hosted on Maven central.
Currently, this codebase and API is in a semi-stable state, with no major backward-compatible changes planned.
zzspec
will start to use semantic versioning and commit to a stable API starting at v1.0.0 (WIP).
Unit testing is an invaluable tool that should also be used in parallel to zzspec
and other high level tests.
It is very easy to use, useful and gives also good information about a system.
Ideally, your “core domain” should be fully tested, and your “business logic” should be encoded in more pure code, ideally as data.
When relevant, you should leverage great property based testing tools and data generators. E.g. ZIO test, to ensure all edge cases are caught in tests.
When we start using external dependencies in a system like databases, caches, external APIs, etc. You have 3 choices:
- create and use mocks
☹️ - create and use stubs (dummy implementations) 😼
- use real dependencies 👍
Mocks are inherently evil : Mocks are generally painful to write, read, debug and maintain.
They should be avoided when possible, we should use real implementations for most tests to a system. testcontainers is a great library for this purpose, and we use it extensively.
Often mocks are used for lack of better tooling, or simply due to habit (specially if you have Java experience). Lots of developers coming from a traditional enterprisy JDK environment know what we mean.
This tendency of doing one class per file, and creating tests for every single class and every single method along with extensive Mockito ideology.
Easier and simpler tests of the entire system, tests have lower complexity.
Testing a system becomes simpler and we can cover many more “real” edge cases.
Easy to cover 100% of a “user flow” or a “data flow”.
Low chance of false positives (partly thanks to avoiding mocks too). This allows for a good test-driven development approach, and more confidence in product.
Testers require less technical knowledge, programming or IT skills and do not need to learn all nitty gritty implementation details of the system.
More loose coupling from the code means more freedom of implementation + refactor
Please feel free to open a pull request, GitHub issue or reach out to me personally (Joe - jjbigorra@gmail.com).
By contributing, your work will be protected under the GNU Lesser General Public License v3.0.
sbt publishSigned
-> sbt sonatypeBundleRelease