50.003 - Specification-based Integration Testing and System Testing¶
Learning Outcomes¶
- Identify the difference between decomposition-based integration testing and call graph-based integration testing.
- Conduct integration test using Jest.
- Derive system testing test cases based on the user case documentations.
- Develop testing strategies based different software development life cycle.
Levels of testing¶
Recall
Design abstraction level | Testing level |
---|---|
Requirement Specifications | System testing |
Preliminary Design | Integration testing |
Detailed Design | Unit testing |
- Unit testing - test individual smallest units of the system.
- Integration testing - test related units/subcomponents of the system, which are related (according to the system design)
- System testing - test the system as a whole, (often according to the user cases).
In this unit, we study integration testing.
Integration Test¶
There are two main approaches of performing integration tests
- Decomposition-based testing
- Call graph-based testing
Decomposition-based testing¶
In Decomposition-based integration testing, we follow the modular structure of the system design.
graph
Program-->Function1
Program-->Function2
Program-->Function3
Function1-->SubFunction1
Function1-->SubFunction2
Function2-->SubFunction3
Function2-->SubFunction4
Function3-->SubFunction5
We perform integration test by following the structure. There are two possible directions.
1. Top-down integration testing
1. Bottom-up integration testing
Top-Down Integration testing¶
In top-down decomposition-based integration testing, we mock up all the sub-components below the main program, and test the main program.
graph
Program-->Function1*
Program-->Function2*
Program-->Function3*
Function1*-->SubFunction1*
Function1*-->SubFunction2*
Function2*-->SubFunction3*
Function2*-->SubFunction4*
Function3*-->SubFunction5*
We put an asterix to denote that function is mocked.
After the main program being tested against the mocked functions, we start to replace the mockded codes with the actual codes starting from the first left child until the bottom right child following the breadth first search order.
graph
Program-->Function1
Program-->Function2*
Program-->Function3*
Function1-->SubFunction1*
Function1-->SubFunction2*
Function2*-->SubFunction3*
Function2*-->SubFunction4*
Function3*-->SubFunction5*
The rationale of the top-down integration test is that when we encounter an error, the error must be caused by the integration of the newly unmocked code.
Bottom-up Integration testing¶
Bottom-up decomposition-based integration testing starts from the bottom left-most or the right most leaf function. We test the leaf functions by making use of the unit test codes (now we call it the driver code).
graph
Program*-->Function1*
Program*-->Function2*
Program*-->Function3*
Function1*-->SubFunction1
Function1*-->SubFunction2
Function2*-->SubFunction3
Function2*-->SubFunction4
Function3*-->SubFunction5
Note that in the above diagram, the components associated with an asterix are yet to be integrated in the integration test.
We then move up the structure by integrating the parents of the leaf functions, (and using the unit test code). We repeat the process until we reach the top.
graph
Program*-->Function1
Program*-->Function2
Program*-->Function3
Function1-->SubFunction1
Function1-->SubFunction2
Function2-->SubFunction3
Function2-->SubFunction4
Function3-->SubFunction5
By doing so, we need not mock up the code as we can reuse (or modify) the unit-test code as drivers.
Limitation¶
The limitation of decomposition-based testing is that the structure is defined by the lexical structure of the source code (definition structure), which often does not reflect the execution and function call relation.
For example, recall in our Echo App (the restful API version), we have two major components in the app. By following the code structure we have.
graph
app-->EchoRouter
app-->MessageModel
EchoRouter-->get.all
EchoRouter-->post.submit
MessageModel-->all
MessageModel-->insertOne
MessageModel-->insertMany
However, we find that we hardly have code from app to call MessageModel directly. In most of the situation, the call sequence is app→EchoRouter→MessageModel.
Call-graph based integration¶
To address the issue with Decomposition based integration, we define the integration structure by following the call graph. For instance, here is the call graph of our Echo App
graph
app-->EchoRouter
EchoRouter-->get.all
EchoRouter-->post.submit
get.all-->MessageModel.all
post.submit-->MessageModel.all
post.submit-->MessageModel.insertMany
MessageModel.insertMany--> MessageModel.insertOne
Note that insertMany()
is never used.
Now we can apply the similar top-down or bottom-up integration test strategies.
Example¶
We reuse the my_mysql_app
developed in the earlier units.
We start by including jest
and supertest
in the project
and modify package.json
to change
Then we create a sub folder __test__
under the project root folder.
Now we should have a project folder structure as the following
Unit Testing Model message.all¶
First we define a unit test on on models/message.js
's function all()
.
In the __test__
folder we add a test file message.test.js
with the following content
The setup
and teardown
define the setup and tear-down routine of this test suite.
Note that in the actual project, you might consider backing up and restoring the actual table data in the setup
and teardown
functions.
In the test suite, we define only one test.
When we run
we see
Integration Test with Echo Router and Model message.all¶
Next we define a bottom-up integration testing by integrating the path from get.all
to
message.all()
.
graph
app-->EchoRouter
EchoRouter-->get.all
get.all-->MessageModel.all
In the __test__
folder we define a new test file echo.test.js
with the following content
The setup and teardown routines are similar to the unit test for message.all()
.
The only difference is that in the test case, we initiate the call from the app level which trigger the router handler with URL path /echo/all
. We then extract the returned text returned from the handler, and parse it back to a json object. Finally we compared the received results (created from json
) and the expected result.
When we run
we see
Cyclic call-graph¶
In case that the call-graph contains cycles, i.e. due to mutual recursion, we have to test the strong connected components as a unit
Pairwise testing¶
Besides top-down or bottom-up strategies, an alternative is to perform pair-wise testing. The idea is to test each edge of the call-graph. Similar to bottom-up strategy, we could convert unit tests for invidiual unit into test drivers for every pair, saving some effort in mock-up effort. One advantage of pairwise testing is to higher degree of fault isolation.
System Testing¶
System testing is often less formal compared to unit testing and integration test. Test cases of the system testing can be derived from
- The use case documents and use case diagrams
- The sequence diagrams
- The state machine diagrams
For instance given the following use case document
Use case ID | UC 1 |
---|---|
Use case name | Create New Message |
Objective | The user creates a new message which will be stored in the Echo app DB |
Pre-conditions | nil |
Primary Actor | User |
Secondary Actor | nil |
Normal Flow | 1. User navigates to https://localhost:3000/echo/ |
2. User enters a new text message and submits | |
3. Echo App receives the message and inserts it into the database | |
4. Echo App returns the list of all messages in the database and display in the UI | |
Alternative Flow | |
Post-conditions | nil |
We can define a system test case as follows
Test case ID | TC 1 |
---|---|
Test case name | Create New Message |
Objective | The user creates a new message which will be stored in the Echo app DB |
Pre-conditions | nil |
Event Sequence | |
Input | User navigates to https://localhost:3000/echo/ |
Output | The new message form is displayed |
Input | User enters a new text message and submits |
Output | The list of messages in the system is returned and rendered, which includes the newly submitted message. |
Post-conditions | nil |
Life-cycle based testing¶
In this section, we discuss how to incoporate the testing activities along with the software development life-cycle.
Waterfall testing¶
In Walterfall software life-cycle, we could easily incoprate the testing activities as the last few phases.
graph
A("Requirements Specification") --> B
B("Analysis") --> C
C("Design") --> D
D("Coding") --> E
E("Unit Testing") --> F
F("Integration Test") --> G("System Test")
As highlighted in the earlier lesson, Waterfall testing as part of the waterfall development life-cycle, suffers from the long feedback interval issues.
Iterative Life Cycle testing¶
In Iterative Software Dvelopment Life Cycle, we break and stage different parts/levels of the system components to be developed in different iterations.
graph
Z("Previous Iteration") --> A
A("Specification") --> B
B("Analysis") --> C
C("Design") --> D
D("Coding") --> E
E("Unit Testing") --> F
F("Integration Testing") --> G
G("Regression Testing") --> H
H("Progression Testing") --> I("Next Iteration")
In terms of testing, we follow a similar structure of waterfall testing for each iteration, except that towards the end, we conduct regression testing and progression testing instead of system test.
- Regression testing - to re-test the test cases defined and passed in the previous iterations.
- Progress testing - to pre-test the test cases defined in the upcoming iterations, some of them should fail.
Agile Testing¶
Recall in Agile development, the development plans are engineered to focus
- Customer-driven
- Bottom–up development
- Flexibility with respect to changing requirements
- Early delivery of fully functional components
Agile Development is often divided into sprints. In each sprints, development team liaise with the project users to identify the deliverables that should be delivered in the particular sprint. In the testing aspect, the testing must be aligned with the user story development for each sprint.
graph
A("Customer Expectation")-->B
B("Iteration Plan")-->C
C("User Story")-->D
D("Design")-->E
E("Coding")-->F
F("Unit Testing")-->G
G("Integration Testing")-->H
H("Regression Testing")-->C
Futher Reading¶
https://lambtsa.medium.com/rest-api-with-express-router-jest-and-supertest-10832a23016f
https://medium.com/geekculture/testing-express-js-with-jest-8c6855945f03