When a project reaches a certain level of complexity, it is prone to all kinds of logic bugs, and these bugs can be repeated in subsequent code changes. Therefore, we need to write various tests for the project to help us control the stability and quality of the project code. Depending on the Test object, we usually have Unit tests for logical code and UI tests for software interfaces.
Unit tests, which test functions or classes, or logical units. Unit testing is based on the theory that a correct function that conforms to expectations has a correct output for each input. Based on this, we can conclude whether the function meets expectations by entering multiple parameters into the test function and judging the output results
Write a Unit Test
Install test frame
Introduce the DART test framework test or use the Flutter_test provided by default with the Flutter scaffolding. The Test framework contains only the apis required for DART testing and is suitable for pure DART projects. Flutter_test contains all apis of the test framework and belongs to the relationship between inclusion and inclusion. However, Flutter_test does not directly depend on the test framework. We usually just select Flutter_test.
dev_dependencies:
flutter_test:
sdk: flutter
Copy the code
Create test files
Normally the test files should be located in the test folder that is placed at the root of the Flutter application or package. The test file is usually named _test.dart, as is common practice for test runners looking for test files. If we run a command to execute all the test files in a folder, the command will execute only the files ending in _test. After the file is created, the file directory structure is as follows:
.├ ── lib │ ├─ CounterCopy the code
Write logical function code
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
Copy the code
Write test code, familiar with test framework API
// Skip the test file, used to temporarily skip a test without alerting
@Skip('currently failing')
// By default, each test will report a timeout error 30 seconds after the activity is stopped. You can set the timeout period manually
@Timeout(const Duration(seconds: 45))
void main() {
final counter = CounterProvider();
// group defines a set of tests that can contain multiple tests. In general, we can take a functional unit as a group, with multiple test boundaries inside, such as the case of correct parameters and the case of wrong parameters.
group(
'Counter',
() {
// Each test case needs to be wrapped with a test to create a test environment. Test can pass in various test environment parameters, such as platform and timeout.
test('value should start at 0', () {
// Expect is the most basic way to tell if two values are the same
expect(counter.value, 0);
counter.increment();
expect(counter.value, 1);
// Expect's second argument can pass various matchers for judging different conditions
// For example, allOf, to determine whether to satisfy multiple matchers
// Contains, isNot, startsWith, endsWith
expect('foo,bar,baz', allOf([contains('foo'), isNot(startsWith('bar')), endsWith('baz')));// Or match a built-in exception
expect(() => int.parse('X'), throwsFormatException);
// Matches a custom exception
expect(Future.error('oh no'), throwsA(equals('oh no')));
},
// Skip the test
skip: "the algorithm isn't quite right".// Single test and group can also set timeout actual
timeout: Timeout(Duration(seconds: 45)));
// The asynchronous method is also fine
test('value should be incremented', () async {
var value = await Future.value(10);
expect(value, equals(10));
// throw does the same
expect(Future.error('oh no'), throwsA(equals('oh no')));
});
},
// Skip testing the group
skip: "the algorithm isn't quite right"
);
// setUp is run before each test and is generally used to initialize shared code
setUp(() async {
counter.reset();
});
// tearDown is allowed after each test is run. It is generally used to clean up shared code so that other test cases are not affected
tearDown(() async {
counter.reset();
});
}
Copy the code
The Test framework does not have many apis, mainly macher for matching results. You can view all macher’s here.
Execute test code
IDE: Right-click the test file counter_test.dart and click Run. Or just click the Run button to the left of the code. Terminal:
flutter test test/counter_test.dart
Copy the code
API
Matcher List
There aren’t many apis used in Unit Test, but perhaps the most complex is the matcher used to verify matches. These Matchers are complex and numerous, but familiarity with these apis will help us write better test cases.
equals
The object can be a primitive type or a matcher.
// If the comparison object (the second argument) is a primitive type, expect wraps it in equals
expect(1.1);
expect(1, equals(1));
expect('foo', equals(contains('foo')));
Copy the code
Core classes
isEmpty
isNotEmpty
isNull
isNotNull
isTrue
isFalse
isNaN
isNotNaN
Same
Compare the two to see if they are the same instance
expect(counter.value, same(vlue));
Copy the code
anything
Match any value
returnsNormally
hasLength
Check whether the length of the object meets requirements
expect('Foo', hasLength(3));
expect('Foo', hasLength(greaterThan(3)));
Copy the code
contains
Whether the comparison value contains
expect('Foo', contains('F'));
Copy the code
isIn
Whether the value of the comparison is included
expect('Foo', isIn('Food'));
Copy the code
predicate
Use the block to manually check whether the value is correct
expect('foo', predicate((value) => value is String));
Copy the code
More classes
greaterThan
greaterThanOrEqualTo
lessThan
lessThanOrEqualTo
isZero
isNonZero
isPositive
isNonPositive
isNegative
isNonNegative
expect('Foo', hasLength(greaterThan(3)));
Copy the code
The wrong class
Basically wrappers for built-in error types
isArgumentError
isCastError
isConcurrentModificationError
isCyclicInitializationError
isException
isFormatException
isNoSuchMethodError
isNullThrownError
isRangeError
isStateError
isUnimplementedError
isUnsupportedError
Type class
isA
Check whether the types are consistent
expect('Foo', isA<String> ());Copy the code
The List comparison object is an iterable class, such as List
everyElement
Every element matches
expect(['foo'.'hoo'], everyElement(contains('oo')))
Copy the code
anyElement
You only need one element to match
expect(['foo'.'hoo'], anyElement(contains('oo')))
Copy the code
orderedEquals
The order and elements are exactly equal
unorderedEquals
The elements are exactly equal, and the order can be different
unorderedMatches
Elements need to match, and the order can be different
pairwiseCompare
containsAll
The Map class
The matching object is Map
containsValue
containsPair
Digital class
Used to determine whether the matched object is in a range
closeTo
inInclusiveRange
inExclusiveRange
inOpenClosedRange
inClosedOpenRange
The String class compares the object to String
equalsIgnoringCase
Case insensitive comparison
equalsIgnoringWhitespace
Ignore space characters
startsWith
endsWith
stringContainsInOrder
Whether subString is included and in the same order
expect('abcdefghijklmnopqrstuvwxyz', stringContainsInOrder(["a"."e"."i"."o"."u"]))
Copy the code
matches
Matching regular expressions
collapseWhitespace
The rule turns multiple Spaces into a single space to match
expect('abc ', collapseWhitespace('abc '))
Copy the code
The operator class
Similar to! | | &&, used to match multiple matcher
expect('foo,bar,baz', allOf([contains('foo'), isNot(startsWith('bar')), endsWith('baz')));Copy the code
isNot
allOf
anyOf
High order
Internal implementation
Expect mostly does peripheral preparatory work, such as determining whether a scope is in a test block and whether it is skipped. It also determines the type of matcher, wraps base types in equals, and so on.
The most important logic inside the Test framework is not Expect, but Matcher, because Macher hosts all the judgment logic.
Matcher is an abstract class. The most important internal method is bool matches(item, Map matchState). Item is the value we compare against, but matchState is not the benchmark for comparison, it is just a Map for recording the result of the comparison, for debugging and printing errors.
The derived classes of Matcher can be divided into four classes: type-dependent TypeMatcher, type-independent _OrderingMatcher, nested recursive _DeepMatcher, and asynchronous AsyncMatcher.
TypeMatcher implements only one method, which is to determine type consistency. His derived abstract class adds an additional method to verify that two values are equal. Its derivative is the alignment of specific types (String, num), they just need to implement their respective type alignment logic.
Another Matcher that directly inherits from Marcher but is not directly related to the type, such as _OrderingMatcher. It is used to implement comparison scenarios such as greaterThanOrEqualTo, lessThan, etc.
We know that Matcher supports nested uses like allOf([contains(‘foo’), isNot(startsWith(‘bar’)), endsWith(‘baz’)]). _DeepMatcher will recursively parse and compare Matcher.
The last one is a Matcher for handling asynchronous alignment. It takes the result of an asynchronous method or value and compares it with the result.
The Mock data
When writing Unit Test, we often encountered unreliable scenarios such as network requests, databases, methods and data provided by other frameworks. This can cause several problems. Let’s take network requests as an example:
-
Whether the API provided by the server is correct or not is not within the scope of App unit testing. The API provided by the server should be tested and the accuracy should be guaranteed by the server. Therefore, mock data should be used instead of real data in the testing process where such data or methods are used. Otherwise, the results of the App’s Unit Test will be affected by a large number of uncertain factors and the conclusions drawn cannot evaluate the quality of the App itself.
-
Accessing online services or databases can slow down test execution.
-
We need to create boundary conditions during testing to test the robustness of the code. For example, various network errors, timeouts, etc. Using real Web services or databases online to test scenarios that are difficult to cover all possible success or failure. So we need to be able to manually control the return of network requests.
We can do this by overriding the return values of these methods through inheritance or implementation protocols, but Mockito makes it easier to mock methods and variables and artificially control the results based on test cases.
/ / define the cat
class Cat {
bool eatFood(String food, {bool hungry}) => true;
Future<bool> sleep() => Future.value(true);
int age = 6;
}
// Mock cat, at which point MockCat gets all the methods and attributes of cat, except that it gets only the empty method (method signature) and attribute name, and no method implementation or attribute value.
class MockCat extends Mock implements Cat {}
void main() {
// Create a mock entity
final cat = MockCat();
group('test cat', () {
test(' ', () {
// You can call a mock class method, but the method is not implemented, so the return value is null
expect(cat.eatFood(any), null);
expect(cat.eatFood('fish'), null);
// Using the Mockito API, we set the sound method to return true when the argument is fish
when(cat.eatFood('fish')).thenReturn(true);
// Then we call the mock class's sound method to return the set value
expect(cat.eatFood('fish'), true);
Argument matcher can be used to match multiple arguments. See ArgMatcher for details
when(cat.eatFood(argThat(startsWith("dry")))).thenReturn(false);
// If the parameter is a named parameter, use anyNamed to specify the parameter name, because named parameters are not sequential and the mock framework cannot determine the actual parameter
when(cat.eatFood(any, hungry: anyNamed('hungry'))).thenReturn(true);
If you want the method to return asynchronous data, instead of using thenReturn, use thenAnswer to return a block
when(cat.sleep()).thenAnswer((_) => Future.value(true));
// You can mock variables as well
when(cat.age).thenReturn(9);
expect(cat.age, 9);
// Verify that cat.sound('fish') has been called before
verify(cat.eatFood('fish'));
// Verify the number of method calls
verify(cat.eatFood('fish')).called(2);
// Also support matcher
verify(cat.eatFood('fish')).called(greaterThan(1));
// Verify that it has not been called
verifyNever(cat.eatFood(any));
// Verify the call order
verifyInOrder([cat.eatFood("Milk"), cat.eatFood('fish'), cat.eatFood("Fish")]);
// Verify that no methods or arguments are called for cat
verifyZeroInteractions(cat);
// Verify that any methods or arguments have been called after cat
verifyNoMoreInteractions(cat);
// Recreate the mock class
reset(cat);
});
});
}
// Inheriting Fake override methods is another way mockito provides you with additional implementations of mock object methods
Mock and Fake cannot be used together. An object that inherits a Mock cannot override the original class method, and an object that inherits a Fake cannot use the return value of the when Mock method.
class FakeCat extends Fake implements Cat {
@override
bool eatFood(String food, {bool hungry}) {
print('Fake eat $food');
return true; }}Copy the code
Best practices
The Mock object is definitely not the object we need to test directly, because the Mock return value is manually set, there is no need to test. We test classes and methods that call Mock objects indirectly, so it is officially recommended that we call the Verity method to verify that the Mock method or variable is invoked after we Mock a method or property of the object. If the object we test doesn’t use any of the Mock object’s methods or variables at all, our Mock becomes meaningless.
The principle of
Mock class implements the primitive class. Mock class implements the primitive class’s method and attribute definitions, but none of them are implemented. Therefore, calling mock class implements noSuchMethod by default. The inherited Mock class implements the noSuchMethod method, replacing the return value of the noSuchMethod with the value we set with when.
Pub. Flutter – IO. Cn/packages/mo…
Code coverage
The official code coverage currently does not support Flutter.
Error handling
Error: Not found: ‘dart:ui’
Since we are a Flutter project, we need to use the Flutter test startup test instead of the PUB Run test. The latter will put the code into the Dart VM for execution, which of course has no Flutter environment.
For IntelliJ or Android Studio, check the Run/Debug Configuration for the IDE. Our test file should not appear in the Dart Command Line App. The Flutter Test is where it belongs.
limited
The Unit Test is testable, and the code is testable. For purely logical methods or classes, Unit Test tests them well and can achieve objective coverage. For example, a method to determine whether a String meets the requirements takes a String and returns a Bool. Providers are also ideal for testing if you use a framework similar to Provider.
However, as projects grow in complexity and code becomes non-standard, we tend to introduce UI logic into purely logical methods or classes, the most common being BuildContext, which is used to pop up toast or jump pages.
There is also the problem of injecting logic that requires mocks into existing projects. In projects that do not introduce TDD, apis are often designed without logic such as dependency injection in mind. This makes it difficult to replace the mock logic later.
Therefore, understanding and becoming familiar with the Unit Test framework and API is only the first step. In the next chapter, we will improve the code step by step and make the untestable code more testable.