Introduction to Testing
Before we start, a look ahead: The next lesson will be along the lines of a paedagogical style called flipped classroom. This basically means that at home, prior to the class, you study what would normally be taught in the classroom, and next time in the classroom we do what would normally be the class’ subsequent assignments/exercises. It is therefore imperative that you carefully and thoroughly work through the following youtube video:
PyTest • REST API Integration Testing with Python
But first things first, on to today’s work.
Test Driven Development
Background
Computer systems must be tested. Hardware, or as in our context, systems, software. This is about testing.
There are several test concepts in system engineering, development, but they all have in common that the test should ascertain that the software does what it is supposed to do. This begs the question: What is it supposed to do?
What is it supposed to do? You will find the answer to that in the specs. This means that there is a proces preceding writing code. The developer gets a system specification explaining in detail what the requirements are. There are usually also a number of use cases involved. Use cases are part of user scenarios.
Use cases are defined in UML, Unified Modelling Language1
Use Cases I
- Requirements in a system entails:
- A need of classes, specific classes.
- A need of functionality, methods of the classes of the system.
- Use cases define the interface to the system.
- Use cases also define the entities of the system. Who needs it?
Use Cases II
- A use case may be high level, quite abstract.
- It might be very detailed.
- It might be a diagram with few words.
- It might be verbose.
- It describes the what of the system.
- The developer uses it to write the how in the form of code.
Use Cases III
- The high level use case:
Use Cases IV
A use case could be verbose:
- Fido barks meaning “I wanna get out!”
- Todd or Gina hears Fido barking.
- Todd or Gina aktivate the remote control.
- The door opens, sesame, sesame …
- Fido exits.
- Fido does what must be done.
- The door closes automatically
- Fido barks to signal “I wanna come back inside!”
- Todd or Gina hears Fido bark (again.)
- One of them activates the remote control (again.)
- The door reopens (again.)
- Fido enters.
- The door closes automatically.
Use Cases V
- Point no 1 was a start condition.
- Subsequently a series of steps illustrate the interface behavior.
- The interface is between an actor and the system.
- A use case ends with an end condition.
- The detailed use case may be very concrete begging detailed code. The end condition must be testable.
Use Cases VI
- A payment system example
Aspects of Testing
Let us look at some focal points of testing by referring to https://www.sitepoint.com/python-unit-testing-unittest-pytest/
We generally distinguish between different levels of software testing:
- Unit testing
- testing specific lines of code, functions, etc.
- Integration testing
- tests integration between units
- System testing
- tests the entire system.
- Acceptance testing
- checks the compliance with business goals as expressed in specs.
Generally speaking the former two are done by the developers, while the latter two involve testers and users.
Test Strategy
Apart from the professional pride of the developer, if that still exists, there are some overall limits that should be considered for a strategy. They include:
- Risks
- What are the business consequences if a bug were to affect this component?
- Time
- How soon must you deliver? Do you have a deadline?
- Budget
- How much money are you willing to spend on testing?
Risk, time, and budget are factors not just in software development, but are general project management topics.
Unit Test Characteristics
The main characteristics of unit tests are
- Speed
- Unit tests are mostly executed automatically, which means they must be fast. Slow unit tests are more likely to be skipped by developers because they don’t provide instant feedback.
- Isolation
- Unit tests are standalone by definition. Each test tests one thing. They don’t depend on anything external like a file or a network resource, if they depend on any other function, they are by definition an integration test.
- Repeatable
- Unit tests are executed repeatedly. They are contained in a program that you run.
- Reliable
- Unit tests themselves are tested before use. They may fail only if there’s a bug in the system being tested. They must be independent of environment, or the order of execution.
- Naming
- Proper naming is as essential in tests as in software development generally. The name of a test should be clear and unambiguous.
Test Pattern
Sitepoint advocates what they call the AAA pattern.
The Arrange, Act and Assert pattern is a common strategy used to write and organize unit tests. It works in the following way:
- During the Arrange phase, all the objects and variables needed for the test are set.
- Next, during the Act phase, the function/method/class under test is called.
- In the end, during the Assert phase, we verify the outcome of the test.
This strategy provides a clean approach to organizing unit tests by separating all the main parts of a test: setup, execution and verification. Plus, unit tests are easier to read, because they all follow the same structure.
Now, let us proceed in more concrete direction.
TDD, Test Driven Development Overview
Computer systems must be tested. Hardware, or as in our context, systems, i.e. software. This section is about testing.
Wikipedia has the following about test-driven development. It cites Beck022 for this and for being the father of TDD. The quote has been cleaned from Wikipedia internal links. The Wikipedia article is very well referenced. Use the link above.
- 1. Add a test
- The adding of a new feature begins by writing a test that passes if and only if the feature’s specifications are met. The developer can discover these specifications by asking about use cases and user stories. A key benefit of test-driven development is that it makes the developer focus on requirements before writing code. This is in contrast with the usual practice, where unit tests are only written after code.
- 2. Run all tests
- The new test should fail for expected reasons This shows that new code is actually needed for the desired feature. It validates that the test harness is working correctly. It rules out the possibility that the new test is flawed and will always pass.
- 3.Write the simplest code that passes the new test
- Inelegant or hard code is acceptable, as long as it passes the test. The code will be honed anyway in Step 5. No code should be added beyond the tested functionality.
- 4. All tests should now pass
- If any fail, the new code must be revised until they pass. This ensures the new code meets the test requirements and does not break existing features.
- 5. Refactor as needed, using tests after each refactor
- Code is refactored for readability and maintainability. In particular, hard-coded test data should be removed. Running the test suite after each refactor helps ensure that no existing functionality is broken.
- Examples of refactoring:
- moving code to where it most logically belongs
- removing duplicate code
- making names self-documenting
- splitting methods into smaller pieces
- re-arranging inheritance hierarchies
- Repeat
- The cycle above is repeated for each new piece of functionality. Tests should be small and incremental, and commits made often. That way, if new code fails some tests, the programmer can simply undo or revert rather than debug excessively. When using external libraries, it is important not to write tests that are so small as to effectively test merely the library itself, unless there is some reason to believe that the library is buggy or not feature-rich enough to serve all the needs of the software under development.
Unit Testing with unittest
Test Functions
Steps 1 and 2
Create the unittest and run it before there’s anything to test. First rule of TDD.
coursecode/pyth/project0/tests.py:
|
|
Execute the test by
|
|
Steps 3 and 4
Second rule of TDD:
coursecode/pyth/project0/lib0.py:
|
|
Execute the test by
|
|
Expand Test
Create the first unittest
and run it.
coursecode/pyth/project1/tests.py:
|
|
Execute the test by:
|
|
Unit Testing with unittest
Test a Class
First create the unittest
and run it.
coursecode/pyth/project20/tests.py:
|
|
Execute it with
|
|
Then we write the class to test with its constructor:
coursecode/pyth/project21/Person.py:
|
|
This is the minimal requirement to satisfy the test. Now test again:
|
|
Unit Testing with pytest
Test Functions
Steps 1-4, Demo
Create the pytest
and the function to test.
coursecode/pyth/project4/test_sample.py:
|
|
The execution of the test
|
|
Expand a Test
Again, create a pytest
and run it.
coursecode/pyth/project41/test_sample.py:
|
|
Execution
|
|
Test a Class
Steps 1 and 2
Create the pytest
and run it before there’s anything to test.
First rule of TDD.
coursecode/pyth/project50/tests/test_distance.py:
|
|
Execute:
|
|
Steps 3 and 4
Second rule of TDD. Make just enough to satisfy tests.
coursecode/pyth/project51/Point.py
|
|
Execute from relevant directory
|
|
Expand the Test
Improving/maintaining software calls for additional tests. We add a test
coursecode/pyth/project52/test1/test_2.py
|
|
Execute from the CLI by
|
|
Do Not Forget
to watch the video before continuing your study of this material.
-
Kent Beck. Test-driven Development by Example. Vaseem: Addison-Wesley, 2002 ↩︎