Some classes of tests, such as integration tests, functional/acceptance tests and performance tests require testing against proper integration environments in order to validate system features. In practice, those environments have a combination of the following characteristics:
In those cases, continuous integration can be seriously impaired. As tests would be frequently broken and their ability to pass won’t be completely dependent on the changes performed to the system, chasing problems may be difficult.
To mitigate the problem those types of environments pose to testing, a stability reference is required to allow for:
The stability reference environment requires an extra stage for the build pipeline, which is completely standalone as it doesn’t depend on any external environment.
The following implementation approaches can be used to allow for integration and function testing outside the actual integration enviroment:
Test doubles require the creation of artifacts which contain embedded test code. This is not desirable, as it adds complexity to the build process and move the build pipeline away from a single-artifact discipline. Also, by not exercising the stack required to interact with external systems, test doubles add very limited value for integration and functional testing.
Servers that provide the same behavior and protocols as the actual integration environment are preferable for the stability reference. They may require the creation of custom software in order to replicate the same semantics as the integration servers, but they have the advantage of exercising the whole integration stack and do not require testing code to be built into artifacts (which is a good practice for build pipelines and testing in general).
These servers are loosely referred to as stubs, mocks, fakes, simulators, and so on, but in most cases these names do not help understanding what is the testing architecture in use. As a matter of fact, those names come from the unit testing space, in which compatibility is the primary goal in order to isolate and test behavior, whereas for integration and functional tests, compatibility, speed, reliability and non-intrusiveness are key.
To avoid naming ambiguities and establish specific semantics, those types of testing servers are going to be referred to as impersonators here. Also, it is assumed from here on that the stability reference is comprised by an impersonated environment.
Simply put, impersonators are testing servers that provide the same data, protocol and semantics as their integration environment counterparts. Impersonated environments (or standalone environments) are comprised exclusively of impersonators and do not depend on external servers. Impersonators may or may not run in the application space, but they are always externalized from the application code.
Impersonators can be implemented using one of the following strategies:
Local deployments of production-compatible servers are sometimes convenient, but they require developers to install and configure servers in their machines (or depend on a single development sever, which imposes a single point of failure) in order to perform changes to the system. This may or may not be a problem, depending on the installation complexity and the amount of resources required to have those server performing well enough for testing. They do have the advantage of providing compatibility out-of-the-box.
Protocol-”compatible” stock servers generally run in memory and perform well, but may have compatibility problems which are hard to predict at the beginning of the development process. They are usually chosen to be used as in-memory servers, which do not require local installations.
Home-grown servers may be challenging to build and/or setup. They are usually not too complicated for read-only systems. However, transactional systems require state management, which require a careful strategy to differentiate and record transaction data. On the other hand, they are in full control of the developers, and are generally built to be lightweight.
An important directive for creating impersonated environments is that all impersonators have to have their own integration tests in order to guarantee they do behave as expected and remove the possibility a flaw in their implementations would cause the main application code to fail.
If testing against integration environments is not guaranteed to work consistently, it is advisable to perform local builds against the standalone environment only, running all the tests against it (instead of using tricks such as smoke builds) as build scalability is achievable in that environment.
If the impersonated environment is properly implemented, builds against that environment should never break, which means there should be no excuse to have the stages in the pipeline associated with the standalone environment broken at any given point in time.
Testing against the actual integration environment is something that can be safely deferred to later stages in the pipeline. If tests are found to be broken there, one of the following possibilities should apply:
Throughout development, most broken builds would be related to reasons 2 and 3. The stability reference would provide means to identify the root causes for the broken tests and eliminate the need to look into the application code as a likely source of problems.
In order to check if the data provided by the impersonators match the data provided by the integration servers, it is convenient to implement a verify feature in each impersonator in order to validate their data and quickly identify if tests were broken due to data variances in the integration environment.
Standalone environments are sometimes useful for showcases, as they are reliable. Depending on the strategy used to implement impersonators, they can run in machines disconnected from the development environment and allow for off-site showcases.
If impersonators do not require local software installations, the costs and risks associated to development environments setups can be drastically minimized, as a single checkout of the codebase would be sufficient to allow for developing changes in the system.
]]>Any feature added to any system has to pass a basic test: If it adds complexity, is the benefit worth the cost? The more obscure or minor the benefit, the less complexity it’s worth. Sometimes this is referred to with the name “complexity budget”. A design should have a complexity budget to keep its overall complexity under control.
Ken Arnold, Generics Considered Harmful
The idea can also be used to to decide which technical aspects are to be prioritized for a given software development strategy.
It seems that focusing too much on some technical aspects and forgetting others is most likely to cause a lot of pain to any project. The question is: which aspects, if dealt with correctly, are more likely to contribute to the success of a given project?
To answer that question, consider the following two projects:
If you had to pick one of those projects to work for, which one would it be? If you’re into actually delivering results, you’d probably pick project 2, as it provides the infrastructure to develop features in a sustainable way. If you’re into RDD and/or if you don’t really care about the actual solution you’re building, you’d probably lean towards project 1.
With that in mind, observe the following technical aspects dependencies graph:

This basically means that focusing on continuous integration (as in: the ability to quickly make sure the system is ready for production) would force the other aspects to be consistently managed. On the other hand, it doesn’t seem to be the case that spending most of the project’s complexity budget into basic aspects (such as dealing with programming languages and playing with frameworks) and leaving the other aspects aside would create an efficient development model.
It’s troubling to see that so much attention in most projects is given to technical aspects that have limited influence on software quality, whereas aspects such as proper testing and continuous integration are taken for granted and not prioritized. Maybe that’s because the most immediate problems found in developing software are related to coding, leading to a strong focus on “instant gratification” topics, such as programming languages, frameworks, code design, etc. At the same time, it may be counter-intuitive to imagine that “good code” could be produced by focusing on the ability to have the system ready for production at any given point in time.
As far as technical concerns influence the solution quality, your best bet to get a project to succeed is to limit its complexity budget by making sure every single technical choice supports a mode of development that is oriented to quickly guarantee the system is always working. Stay away from the initial temptation to aimlessly play with tools, programming languages and frameworks; focus on continuous integration first, derive the other choices from that, and you won’t get it wrong.
]]>