Quality Assurance: TDD
TDD?
Test-driven development (TDD) is a software development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases.
Source: https://en.wikipedia.org/wiki/Test-driven_development
TDD is the reverse of the traditional development then testing approach. So, instead of writing code for development first, we first create test cases based on the requirements of the product then code until it passes all the test cases made. This will result in a cleaner and less prone to breaking code for the long run.
Why TDD?
Here are the benefits we get by using TDD in our software development process:
Reduce Bugs
Since tests are made to reveal potential issues that will likely surface in the future, it is more likely that bugs will be noticed in the test case.
Our Code Guardian
The tests we make will be a guardian to our implemented code. This is useful to prevent whenever breaking existing code after we write new code. If somebody else in our team writes new code that breaks our code, the test that we made could fail as it is no longer working as intended. Having a failing test means that the code cannot go into production. This results in making developers develop without the fear of breaking code.
Code Documentation
The test cases we make are like stories to our team of developers. The tests help others understand the flow of our implemented code since tests are made to specify what our code should be doing. In addition to that, tests are made for various scenarios. So whenever others in our team wants to make adjustments or use our code, they know the ins and outs of what our code is intended to do.
TDD Phases
There are three main phases of TDD:
- RED, is the phase where we make our test cases based on the requirements. This phase is called RED and is red in color because it represents failure where the test cases we make will always fail since we haven’t implemented the code that passes the test cases.
- GREEN, is the phase where we implement code that passes all the test cases made on the RED phase. This phase is called GREEN and is green in color because it represents success where our code that we just implemented passes all the test cases.
- REFACTOR, is the phase where we are confident in our code that it is covered with tests but still want to improve our code by following the clean code principle.
Coverage
Coverage is measure as in to what degree our code is executed after performing tests. A high coverage software means that most of its code are executed during test. Therefore, there is a lower chance of a bug surfacing than a software that has a lower coverage.
The main criteria of coverage are:
- Function coverage, which assess how much functions (or subroutines) called.
- Statement coverage, which assess how many statements are executed.
- Edge coverage, which assess every edge of the program are executed.
- Condition coverage, which assess every boolean statement of the program are executed whether it is true or false.
Implementation
Here’s a TDD example on a mobile app I am currently working on. Since I already have a prototype design, I will mostly refer to that and make tests based on it since it’s made according to the backlog. So here’s the design:
The words are in Indonesian if you are curious. So above, I’ve labeled them as ‘First state’ and ‘Second state’ where the first state is the state of the form before clicking on the ‘upload’ button and the second is the state of the form after an image has been uploaded.
After having a quick glance over the design, we should have a rough understanding of the form:
- When we open the form, we should expect a
Tambah Laporan Pengembalian Uang
text,Deskripsi Pengembalian Uang
text, a button withUpload
as a label, a button withBatal
as a label, and a button withSimpan
as a label. - After uploading the image, it should show the filename of the uploaded image.*
- The
Batal
andSimpan
button should be clickable. - The
Batal
button should close the screen. - The
Simpan
button should save the object to the database.*
*Ignored because not integrated with backend yet.
So now, we should create the test based on our understanding above. Let’s name the screen CreateMoneyReturn
in the file create_money_return.dart
, so the test should be create_money_return_test.dart
. Here’s an example of the test I made.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:simpk/config/theme.dart';Future _pumpTestableWidget(WidgetTester tester, Widget child) async {
ThemeData theme = appTheme();
await tester.pumpWidget(MaterialApp(home: child, theme: theme));
}void main() {
testWidgets('Create money return report: initial state'
(tester)async {
await _pumpTestableWidget(tester, Scaffold(
body:CreateMoneyReturn())
);
expect(
find.text('Tambah Laporan Pengembalian Uang'),
findsOneWidget
);
expect(
find.text('Deskripsi Pengembalian Uang'), findsOneWidget);
expect(find.textContaining('Upload Foto'), findsOneWidget);
expect(find.text('Batal'), findsOneWidget);
expect(find.text('Simpan'), findsOneWidget);
}); testWidgets('Create money return report: Tap Batal to go back'
(WidgetTester tester) async {
await _pumpTestableWidget(tester, Scaffold(
body:CreateMoneyReturn()));
await tester.pump();
await tester.tap(find.text("Batal"));
await tester.pumpAndSettle(); expect(find.byType(Scaffold), findsNothing);
}); testWidgets(
"Create money return report: Simpan enabled when description is filled",
(tester) async {
await _pumpTestableWidget(tester, CreateReport1Screen());
ElevatedButton btn = tester.firstWidget(find.byType(ElevatedButton).last);
expect(btn.enabled, false); await tester.enterText(
find.byType(TextFormField).first,
"Suatu gambar pengembalian uang");
await tester.pump(); btn = tester.firstWidget(find.byType(ElevatedButton).last);
expect(btn.enabled, true);
});group('$ImagePicker', () {
const MethodChannel channel =
MethodChannel('plugins.flutter.io/image_picker'); final List<MethodCall> log = <MethodCall>[]; setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
return '';
}); log.clear();
}); testWidgets(
'Add Attachment: Take image for choosing image works'
(WidgetTester tester) async {
await _pumpTestableWidget(tester,
Scaffold(body:CreateMoneyReturn())); await tester.tap(find.textContaining("Upload Foto"));
await tester.pump();
expect(
log,
<Matcher>[
isMethodCall('pickImage', arguments: <String, dynamic>{
'source': 0,
'maxWidth': null,
'maxHeight': null,
'imageQuality': null,
'cameraDevice': 0
}),
],
);
});
});
}
For privacy reasons, the implementation of the test will not be shown. But the test above should cover most of the points stated above. I made sure to make the description as clear as possible since I am not the only on working on this project. A test basically is like storytelling to other people. So whenever another developer doesn’t understand what we coded, they can simply take a look at the test before asking directly to the one that made the code.
Conclusion
TDD is a powerful tool for developers since the benefits it gives helps developers into writing better code. Reducing bugs, cleaner code, acting as a code documentation and code guardian, and increase the confidence in developing more code is just a good trade for the cons it has. Even though it may be overwhelming by newer developers, it is better to follow the TDD approach for software developing in the long run.