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:

  1. 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.

  2. Accessing online services or databases can slow down test execution.

  3. 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.