Herramientas de usuario

Herramientas del sitio


wiki2:engineering:testing

Testing practices and strategies

Types of testing

  • Acceptance testing. Tests that ensure a user perspective is fulfilled.
  • Unit test. They test that a single piece of code (function, class, module…) fulfills its requirements.
  • Integration tests. Test the collaboration between components.
  • Regression tests. Those which ensures that previously developed features are still working.
  • Performance tests. It test execution speed, memory usage, and other performance metrics.
  • Stress tests. Test the system response under adverse conditions (failure of components, attacks…).
  • Load tests. Test to check the performance of the system under a high workload.

  • Smoke testing: a software testing process that determines whether the deployed software build is stable or not. It's usually done when a build is released.
  • Sanity testing: performed after receiving a software build, with minor changes in code, or functionality, to ascertain that the bugs have been fixed and no further issues are introduced due to these changes. The goal is to determine that the proposed functionality works roughly as expected. If sanity test fails, the build is rejected to save the time and costs involved in a more rigorous testing.
  • Sometimes smoke and sanity are defined as synonyms.
  • Pre-flight check: Tests that are repeated in a production-like environment, to alleviate the 'builds on my machine' syndrome. Often this is realized by doing an acceptance or smoke test in a production like environment.
  • Functional testing: validates the software system against the functional requirements/specifications. The purpose of Functional tests is to test each function of the software application, by providing appropriate input, verifying the output against the Functional requirements.

Testing principles

Tests help you to understand the code as they are minimum uses well and self explained of atomic actions.

When a bug appears a new test, replicating the problem, should appear too. It should fail, so once you fix the problem it should pass and remain there. This is a powerful and useful technique. If you find a bug, write a test that reveals it. Then, you can quickly fix the bug by debugging the test. This turns into a good regression test.

Tests are first and foremost a design tool. Code which is hard to test is likely hard to read, reason about and maintain.

When a test is hard to write, when your a test is brittle (breaking often when the code changes), then you've probably written bad code. Tests are a code quality, not code correctness, metric.

You should be able to take your unit test and run it on another's computer even when it's not connected to the internet.

Follow the AAA rule (Arrange, Act, Assert). This is a general pattern for make tests more readable and useful. (1) You arrange, set variables, properties and all required things for the test to run with the spected result. (2) You act, call the method you are testing. And (3) you assert verifying the given result.

First write the happy-path. Those that are easy. Then, when they pass, look for the edges and boundaries of your code.

If possible, cover every code path.

Name your tests clearly, do not be afraid of long names. It's a really good practice to add good comments\documentation to explain why the test exists and what is being tested.

Test that every raised exception is raised.

Set up a clean environment for each test. Previous run tests can let some residual data that can make current tests fail; even worse, it can happen that your tests depend on that data.

If it takes too much code to setup a test, it’s likely that the unit you want to test is too complicated and would benefit from refactoring. The more code you need to write in the setup part of the unit test, the tighter coupling occurs between the test and its unit, which leads to maintenance headaches in case the unit code starts changing.

Unit tests have to be FIRST:

  • Fast. Execute in few seconds or less.
  • Isolated, only one piece of code at a time.
  • Repeatable, all reruns give the same outcome.
  • Self-verifying, does not need additional information to evaluate the test.
  • Timely, tests are written before the code.

Write tests when know the answers before to some complicated code.

Try to test over pure-functions.

The more you have to mock out to test your code, the worse your code is. The more code you have to instantiate and put in place to be able to test a specific piece of behavior, the worse your code is. The goal is small testable units, along with higher-level integration and functional tests to test that the units cooperate correctly.

They test individual software units, which can be individual classes, whole aggregates, or whatever else fits. Since we’re only interested in the proper behavior of our unit, we should isolate external dependencies such as databases, remote systems, etc. Hence, we say that unit tests are performed in isolation.

Tests should NOT be fragile. Test should not fail without reasons.

Other properties that tests should follow:

  1. Structural independence: To be structural independent your test result should not change if the structure of the code changes.
  2. Behavioural dependence: To be behavioural dependent a test case result should change when the behavior of the code under test change.

Testing strategies

Better Than Unit Tests - Article

Automated contract

  • The contract model should be written by a consumer to express only the parts of the service interface they care about. (If you overspecify by modeling things you don't actually use, then your tests will throw false negatives.)
  • The supplier should not write the contract. Consumers write models that express their desired interface partly to help validate their understanding of the protocol. They may also uncover cases that are in the supplier's blind spot.
  • The test double should not make any assumptions about the logical consistency of the results with respect to the parameters. You should only be testing the application code and the way it deals with the protocol.
  • E.g., if you are testing an “add to cart” interface, do not verify that the item you requested to add was the one actually added. That is coupling to the implementation logic of the back end service. Instead, simply verify that the service accepts a well-formed request and returns a well-formed result.

Property-based test

It uses a model of the system to describe the allowed inputs, outputs, and state transitions. Then it randomly (but repeatably) generates a vast number of test cases to exercise the system. Instead of looking for success, property-based testing looks for failures. It detects states and values that could not have been produced according to the laws of the model, and flags those cases as failures.

Fault injection

You run the system under test in a controlled environment, then force “bad things” to happen. These days, “bad things” mostly means network problems and hacking attacks. I'll focus on the network problems for now.

Run the system into a bunch of VMs, then generate load. While the load is running against the system, introduce partitions and delays into the virtual network interfaces. Introducing controlled faults and delays in the network, lets us try out conditions that can happen “in the wild” and see how the system behaves.

Simulation

In simulation testing, we use a traffic model to generate a large volume of plausible “actions” for the system. Instead of just running those actions, though, we store them in a database.

Some notes

Anti-patterns

When we are just asserting that some methods are called in a specific order with specific parameters. This feels almost like testing that the compiler/interpreter works. We've fallen into a trap of testing that the code does what the code says it does, rather than testing functional behavior we care about.

If you can’t make a test smaller, your code probably has too many dependencies.


  1. Having unit tests without integration tests
  2. Having integration tests without unit tests

Test criticism

The key problem with testing is that a test (of any kind) that uses one particular set of inputs tells you nothing at all about the behaviour of the system or component when it is given a different set of inputs. The huge number of different possible inputs usually rules out the possibility of testing them all, hence the unavoidable concern with testing will always be, “have you performed the right tests?” The only certain answer you will ever get to this question is an answer in the negative — when the system breaks.

By definition, unit tests test predictable bugs; or bugs that have already been encountered. They are often reactive, as opposed to proactive.

A good planner can foresee many types of issues, and write unit tests in advance, to validate the code that addresses them, but, by definition, this is “low-hanging fruit.”

No matter how good I am, I WILL miss bugs; sometimes, really bad ones. I have been constantly surprised by the bugs that crop up in projects that I would have SWORN were “perfect.”

Coverage value

This value tells you the percentage of code which is executed during test. However, it does not mean that all that code is tested.

Generally that percentage is “the happy-path”, the best test scenario.

Thus, code coverage value does not rate the code correctness.

Tips for good testing

  • Keep test code very simple.
  • Avoid multiple assertions in the same test: many small tests are better than few big ones.
  • Avoid multiple loops or ifs.
  • Remember: we don't want to debug two programs at the same time (the program and the test).
  • On test classes prefer composition over inheritance. To create a base class for tests is like creating a black hole of unrelated shared code which will grow quickly.
  • Make them to run fast.
  • Make them deterministic.
  • Test your tests; not writing tests for them but using practices like mutation testing.
  • When a test fails it should be easy to find the reason.

Notes

Unit tests could run parallelized.

wiki2/engineering/testing.txt · Última modificación: 2020/08/29 19:08 (editor externo)