I believe that in some small and medium-sized companies in China, developers will rarely write software testing related code. Of course there are some reasons behind this. This article is about software testing in iOS development.
First, the importance of testing
Testing is important! Testing is important! Testing is important! It’s so important that it should be repeated for three times.
Scenario 1: Every time we write code, we need to compile and run to see if the application performs as expected. If the change point, the amount of code is small, the cost of verification is lower, if it does not meet expectations, it means that our code is problematic, and it takes less time to troubleshoot the problem manually. If there are a lot of changes and a lot of affected places, we first have to guess the affected functions, and then the cost of locating and troubleshooting the problems is very high.
Scenario 2: One of your new SDK sub-features needs a technical refactoring. But you can only find readMes, access documents, system design documents, technical solution evaluation documents, and a bunch of other documents on your company’s internal code hosting platform. Maybe you’ll read it before you refactor it. When you’re done refactoring, you go to the App access test for one of the company’s lines of business, click on it a few times and discover a crash. 😂 thought, the local test, debug are normal but why after access Crash. When you think about it, it’s easy to understand that your local refactoring only ensures that the one feature you’re developing works, and it’s hard to make sure that the code you’re writing doesn’t affect other classes or other features. If the previous SDK had Unit Test code for each class, you could have run the entire Unit Test code once the new feature was developed, making sure every Unit Test passed and branching coverage reached the agreed line, and that would have been fine.
Scenario 3: Plan feature A during the release iteration, from development, commissioning, testing, and launch to 2 weeks. The old driver is very confident in his work. With such simple UI, animation, interaction and coziness of code, the “domain drive” of the reference server has been tested in the development stage of this feature. The joint investigation and local test have passed, and there are still 3 days left. I thought the test will last for 1 day, and the bug fix will last for submission for review. The code plays a trick on you and tests out n bugs (much more than expected). In order not to affect the App launch, I had to stay up late to fix bugs. Will all the test by test engineers to deal with, the quality should be very stable phase theory, otherwise there are leaks in the stage revealed abnormal code, technical design is too late, you need to coordinate each team resources (interface may want to change, the product side want to change), cause at this stage the cost of change is very big.
Most developers have encountered the above scenario. In fact, there is a solution to all these problems, that is “software testing”.
Second, software testing
Classification of 1.
Software testing is the process of operating the application program under specified conditions to find program errors, measure software quality, and evaluate whether it can meet the design requirements.
The three scenarios in Part 1 can be avoided by applying software testing techniques properly.
Software testing emphasizes the synchronous development and testing, or even the test in advance. From the requirement review stage, we should first consider the software testing scheme, and then carry out the technical scheme review, development coding, unit test, integration test, system test, regression test, acceptance test, etc.
Software testing is divided into unit testing, integration testing, system testing, regression testing, acceptance testing (some companies talk about “smoke testing”, the exact definition of the word is not known, but when learning software testing courses, according to the scope of only the above several categories). The engineers themselves are responsible for unit testing. Test engineer, QA is responsible for integration testing, system testing.
Unit Testing: Also known as module Testing, is a test for the correctness of the program module (the smallest Unit of software design). The concept of a “unit” is more abstract. It is not only a method or function that we write, but also a class or object.
Software testing can be divided into two development modes: test-driven development (TDD) and behavior-driven development (BDD).
2. TDD
The idea of TDD is to write test cases first, then develop the code quickly, and then, with the guarantee of test cases, you can easily and safely refactor the code to improve the quality of the application. In short, testing drives development. Because of this feature, TDD is widely used in agile development.
In other words, in TDD mode, we should first consider how to test the function, then write the code to implement it, and then continue to iterate and optimize the code under the guarantee of test cases.
Advantages: Clear goals, clear architecture layers. Ensures that development code does not deviate from requirements. Continue testing at each stage
Disadvantages: The technical solution needs to be reviewed first, and the architecture needs to be built in advance. If the requirements change, the previous steps need to be repeated with less flexibility.
3. BDD
BDD is behavior-driven development, which is one of the agile development technologies. It defines system behavior through natural language and writes requirement scenarios from the perspective of function users, and these behavior descriptions can directly form requirement documents and also serve as test standards.
The idea of BDD is to go beyond a single function to test for behavior. BDD is concerned with the business domain and behavior mode, rather than specific functions and methods, and verifies the availability of functions through the description of behaviors. BDD uses Domin Specific Language (DSL) to describe test cases. The test cases are written in such a way that they are easy to read and look like documents. The code structure of BDD is Given->When->Then.
Advantages: Members of various teams can come together to design behavior-based test cases.
4. Contrast
According to the characteristics that is to find their own use scenarios, TDD mainly for the development of the smallest unit for testing, suitable for unit testing. BDD, on the other hand, is for behavior, so the scope of testing can be larger and can be used in both integration testing and system testing
Test cases written in TDD are typically developed for the smallest unit of development (such as a class, function, method) and are suitable for unit testing.
BDD test cases are written for behavior, and the test scope is larger, which is suitable for integration testing and system testing stages.
Unit test coding specification
The main focus of this article is on what engineers can do in the daily development phase, namely unit testing.
When writing functional and business codes, the kiss principle is generally followed, so classes, methods and functions are usually not too large. The better the hierarchical design, the more single the responsibility and the lower the coupling degree, the more suitable the code is for unit testing. Unit testing also forces the code to be layered and decoupled in the development process.
Maybe a feature has 30 lines of implementation code and 50 lines of test code. How can the code for unit tests be written more reasonably, cleanly, and formally?
1. Expand coding modules
Let’s start with a piece of code.
- (void)testInsertDataInOneSpecifiedTable { XCTestExpectation *exception = [self ExpectationWithDescription: @ "test database insert function"]; // given [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta]; NSMutableArray *insertModels = [NSMutableArray array]; for (NSInteger index = 1; index <= 10000; index++) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = index; / /... [insertModels addObject:model]; } // when [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta]; // then [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) { XCTAssert(count == InsertModels. Count, @" increment "); [exception fulfill];}]; [self waitForExpectationsWithCommonTimeout]; }Copy the code
You can see the title of this method for testInsertDataInOneSpecifiedTable, this code is done by the function name can see: test inserts data to a particular table. The test case is divided into three parts: preparation of prerequisites required for the test environment; Call a method or function to be tested. Verify that the output and behavior are as expected.
In fact, each test case should be written in this way to organize the code. The steps are divided into three stages: Given->When->Then.
So the code specification for unit testing comes out. In addition, once the unit test code specification is unified, everyone’s test code is expanded according to this standard, which makes it easier and more convenient for others to read. Follow these three steps to read and understand the test code, and you’ll know exactly what you’re doing.
2. A test case tests only one branch
The code we write is composed of many statements, with various logical judgments, branches (if… Else, swicth, etc., so a program enters from a single entry point, and the process may produce n different branches, but the program exit is always one. So because of this nature, our tests need to go through as many branches as possible for this situation. The corresponding metric is called branch coverage.
If a method has an if… else… In the test, we try to write each situation into a separate test case with separate input and output to judge whether it meets the expectation. In this way, each case tests a single branch, which is also very readable.
For example, to unit test the following function, the test case design is as follows
- (void)shouldIEatSomething { BOOL shouldEat = [self getAteWeight] < self.dailyFoodSupport; if (shouldEat) { [self eatSomemuchFood]; } else { [self doSomeExercise]; }}Copy the code
- (void)testShouldIEatSomethingWhenHungry
{
// ....
}
- (void)testShouldIEatSomethingWhenFull
{
// ...
}
Copy the code
3. Clearly identify the class to be tested
This is mainly from the perspective of teamwork and code readability. Anyone who has ever written a unit test knows that a function might have 10 lines of code, but 30 lines of test code were written to test it. It doesn’t matter if a method is written this way. You can see which method of which class you are testing. However, when the class itself is very large and the Test code is very large, it will cost a lot to read the code, whether it is the author himself or other colleagues who will be responsible for maintenance years later. You need to see the Test file name, code and class name + Test first to know which class is being tested. Test method name test method name.
Such code is very unreadable, so the current test object should be specially marked so that the more readable the test code is, the less expensive it is to read. For example, define the local variable _sut to mark the class currently under Test.
#import <XCTest/XCTest.h> #import "HCTLogPayloadModel.h" @interface HCTLogPayloadModelTest : HCTTestCase { HCTLogPayloadModel *_sut; } @end @implementation HCTLogPayloadModelTest - (void)setUp { [super setUp]; HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init]; model.log_id = 1; / /... _sut = model; } - (void)tearDown { _sut = nil; [super tearDown]; } - (void)testGetDictionary { NSDictionary *payloadDictionary = [_sut getDictionary]; XCTAssert([(NSString *)payloadDictionary[@"report_id"] isEqualToString:@"001"] && [payloadDictionary[@"size"] integerValue] == 102 && [(NSString *)payloadDictionary[@"meta"] containsString:@"meiying"], @"HCTLogPayloadModel 'getDictionary' is abnormal "); } @endCopy the code
4. Use classes to expose private methods, private variables
In some scenarios, written test methods may need to internally call private methods of the object under test or access a private property of the object under test. However, the private properties and methods of the class under test cannot be accessed in the test class.
Add a class for the test class with the suffix UnitTest. As shown below.
The HermesClient class has a private @property (nonatomic, strong) NSString *name; , private method – (void)hello. To access private properties and private methods in the test case, the following classification is written
// HermesClientTest.m
@interface HermesClient (UnitTest)
- (NSString *)name;
- (void)hello;
@end
@implementation HermesClientTest
- (void)testPrivatePropertyAndMethod
{
NSLog(@"%@",[HermesClient sharedInstance].name);
[[HermesClient sharedInstance] hello];
}
@end
Copy the code
4. Development mode and technical framework selection under unit test
Unit tests are divided by test scope. TDD and BDD are divided according to development mode. Therefore, there are various permutations and combinations, and here we only care about TDD and BDD schemes under unit testing.
In the unit testing phase, both TDD and BDD can be applied.
1. TDD
TDD emphasizes that continuous testing drives code development, which simplifies code and ensures code quality.
The idea is that when you get a new feature, first think about how to test it, various test cases, various boundary cases; Then complete the development of the test code; Finally, write the corresponding code to meet and pass these test cases.
The TDD development process looks like the following:
- First write the function of the test case, to achieve the test code. At this time to run the test, is not passed, that is, to the red state
- Then write the actual functionality implementation code. At this time to run the test, the test passed, that is, to the green state
- With the guarantee of test cases, the code can be refactored and optimized
Throw up a question: TDD looks good. Should you use it?
There is no rush to answer this question, and there is no right or wrong answer. Development is often such a process, after the new requirements come out, first through the technical review meeting, determine the macro level of technical solutions, determine the technical implementation of each end, the use of technology, etc., sorted out the development documents, conference documents. Start coding after time limit assessment. Is it that simple? Even if we think fully and carefully in the early stage, special cases may be missed, leading to changes in technical solutions or technical implementation. If TDD is used, it is necessary to consider the design of test cases, write test code, and then implement the function under the guarantee of test cases after the new function is given. If the technical solution is changed, the previous test case is changed and the test code implementation is changed. A new case may cause most of the test code and implementation code to change.
How to conduct TDD campaign
- Create a new project and make sure the Include Unit Tests option is selected
- The created project directory is as follows
-
Delete the test template file tddDemotests.m created by Xcode
-
Suppose we had to design a human being to eat and say “how full” when he’s finished.
-
So according to TDD we first design test cases. So let’s say I have a Person class, and I have an object method called eat, and when I finish eating, it returns a string that says, “I’m so full.” So the test case is
steps expect The results of Instantiate the Person object, calling the object’s EAT method Call and return “So full” ? -
Implement the test case code. Create a Test class that inherits from the Unit Test Case class and name it tddPersonTest.m.
-
Because you want to test the Person class, create the Person class in the main project
-
To test whether humans can say “so full” after eating. So imagine that class currently has only one feeding method. So in TDDPersonTest. Create a test function in m – (void) testReturnStatusStringWhenPersonAte; The function looks like this
- (void)testReturnStatusStringWhenPersonAte { // Given Person *somebody = [[Person alloc] init]; // When NSString *statusMessage = [somebody performSelector:@selector(eat)]; // Then XCTAssert([statusMessage isEqualToString:@" I'm so full "], @"Person "); }Copy the code
-
Xcode: press Command + U to run the code and find it failed. Because our Person class doesn’t implement the corresponding method at all
-
As you can see from the TDD development process, we are now in the red “Fail” state. So you need to implement the functionality code in the Person class. The Person class as follows
#import "Person.h" @implementation Person - (NSString *)eat { [NSThread sleepForTimeInterval:1]; Return @" so full ";; } @endCopy the code
-
Run the test case again (Command + U shortcut). A test that is found to have passed is a green “Success” status during TDD development.
-
If necessary, do some pre-testing in the -(void)setUp method and resource release in the -(void)tearDown method
-
Let’s say the eat method isn’t implemented pretty enough. Now, under the guarantee of the Test cases, we can boldly refactor and finally make sure that all the Unit Test cases pass.
2. BDD
Compared with TDD, BDD focuses on the design of behavior patterns. Take “people eat” as an example.
Steps 1 to 4 are the same as TDD.
-
BDD requires implementing the functional code first. Create a Person class that implements -(void)eat; Methods. The code is the same as above
-
BDD requires the introduction of a handy framework, Kiwi, using Pod
-
To test whether humans can say “so full” after eating. So imagine that class currently has only one feeding method. So in TDDPersonTest. Create a test function in m – (void) testReturnStatusStringWhenPersonAte; The function looks like this
#import "kiwi.h" #import "Person.h" SPEC_BEGIN(BDDPersonTest) describe(@"Person", ^{ context(@"when someone ate", ^{ it(@"should get a string",^{ Person *someone = [[Person alloc] init]; NSString *statusMessage = [someone eat]; [[statusMessage shouldNot] beNil]; [[statusMessage should] equal:@" how full "]; }); }); }); SPEC_ENDCopy the code
3. XCTest
Development steps
Xcode’s own testing system is XCTest, which is easy to use. The development steps are as follows
-
Create a test class in the Tests directory that inherits from XCTestCase for the class under test.
-
– (void)testPerformanceExample; – (void)testExample;
-
Just like normal classes, you can inherit, you can write private properties, private methods. So you can write some private properties and so on in the new class as required
-
Write some code in the – (void)setUp method to initialize and start the setUp. For example, when testing database functionality, write some database connection pooling code
-
Write test methods for each method in the class under test. There may be n methods in the class under test and M methods in the class under test (m >= N). As we explained in Part 3: Unit Test Coding specification, a test case tests only one branch. When there are if and switch statements inside a method, you need to write test cases for each branch
-
There are specifications for the test methods that are written for each method of the test class. The name must be test+ the name of the method under test. Function has no arguments and no return value. For example – (void)testSharedInstance.
-
The code in the test method is expanded in the order Given->When->Then. Preparation of prerequisites for the test environment; Call a method or function to be tested. Use assertions to verify that the output and behavior are as expected.
-
Write some code inside the – (void)tearDown method to free the resource or close it. For example, when testing database functionality, write some code to close the database connection pool
Assertion dependent macros
/ *! * @function XCTFail(...) * Generates a failure unconditionally. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTFail(...) \
_XCTPrimitiveFail(self, __VA_ARGS__)
/ *! * @define XCTAssertNil(expression, ...) * Generates a failure when ((\a expression) ! = nil). * @param expression An expression of id type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNil(expression, ...) \
_XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertNotNil(expression, ...) * Generates a failure when ((\a expression) == nil). * @param expression An expression of id type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNotNil(expression, ...) \
_XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssert(expression, ...) * Generates a failure when ((\a expression) == false). * @param expression An expression of boolean type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssert(expression, ...) \
_XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertTrue(expression, ...) * Generates a failure when ((\a expression) == false). * @param expression An expression of boolean type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertTrue(expression, ...) \
_XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertFalse(expression, ...) * Generates a failure when ((\a expression) ! = false). * @param expression An expression of boolean type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertFalse(expression, ...) \
_XCTPrimitiveAssertFalse(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertEqualObjects(expression1, expression2, ...) * Generates a failure when ((\a expression1) not equal to (\a expression2)). * @param expression1 An expression of id type. * @param expression2 An expression of id type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertEqualObjects(expression1, expression2, ...) \
_XCTPrimitiveAssertEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertNotEqualObjects(expression1, expression2, ...) * Generates a failure when ((\a expression1) equal to (\a expression2)). * @param expression1 An expression of id type. * @param expression2 An expression of id type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNotEqualObjects(expression1, expression2, ...) \
_XCTPrimitiveAssertNotEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertEqual(expression1, expression2, ...) * Generates a failure when ((\a expression1) ! = (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertNotEqual(expression1, expression2, ...) * Generates a failure when ((\a expression1) == (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNotEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertNotEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) * Generates a failure when (difference between (\a expression1) and (\a expression2) is > (\a accuracy))). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) \
_XCTPrimitiveAssertEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)
/ *! * @define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) * Generates a failure when (difference between (\a expression1) and (\a expression2) is <= (\a accuracy)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) \
_XCTPrimitiveAssertNotEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)
/ *! * @define XCTAssertGreaterThan(expression1, expression2, ...) * Generates a failure when ((\a expression1) <= (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertGreaterThan(expression1, expression2, ...) \
_XCTPrimitiveAssertGreaterThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) * Generates a failure when ((\a expression1) < (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertGreaterThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertLessThan(expression1, expression2, ...) * Generates a failure when ((\a expression1) >= (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertLessThan(expression1, expression2, ...) \
_XCTPrimitiveAssertLessThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertLessThanOrEqual(expression1, expression2, ...) * Generates a failure when ((\a expression1) > (\a expression2)). * @param expression1 An expression of C scalar type. * @param expression2 An expression of C scalar type. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertLessThanOrEqual(expression1, expression2, ...) \
_XCTPrimitiveAssertLessThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
/ *! * @define XCTAssertThrows(expression, ...) * Generates a failure when ((\a expression) does not throw). * @param expression An expression. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertThrows(expression, ...) \
_XCTPrimitiveAssertThrows(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertThrowsSpecific(expression, exception_class, ...) * Generates a failure when ((\a expression) does not throw \a exception_class). * @param expression An expression. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertThrowsSpecific(expression, exception_class, ...) \
_XCTPrimitiveAssertThrowsSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)
/ *! * @define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) * Generates a failure when ((\a expression) does not throw \a exception_class with \a exception_name). * @param expression An expression. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException. * @param exception_name The name of the exception. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) \
_XCTPrimitiveAssertThrowsSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
/ *! * @define XCTAssertNoThrow(expression, ...) * Generates a failure when ((\a expression) throws). * @param expression An expression. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNoThrow(expression, ...) \
_XCTPrimitiveAssertNoThrow(self, expression, @#expression, __VA_ARGS__)
/ *! * @define XCTAssertNoThrowSpecific(expression, exception_class, ...) * Generates a failure when ((\a expression) throws \a exception_class). * @param expression An expression. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNoThrowSpecific(expression, exception_class, ...) \
_XCTPrimitiveAssertNoThrowSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)
/ *! * @define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) * Generates a failure when ((\a expression) throws \a exception_class with \a exception_name). * @param expression An expression. * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException. * @param exception_name The name of the exception. * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. */
#define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) \
_XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
Copy the code
Experience summary
-
XCTestCase classes are like any other class. You can define base classes that encapsulate common methods.
// HCTTestCase.h #import <XCTest/XCTest.h> NS_ASSUME_NONNULL_BEGIN @interface HCTTestCase : XCTestCase @property (nonatomic, assign) NSTimeInterval networkTimeout; / * * set a default time asynchronous test XCTestExpectation timeout handling * / - (void) waitForExpectationsWithCommonTimeout; /** Set @param handler timeout logic for asynchronous tests with a default time */ - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler; / * * generates Crash types of meta data @ return meta types of dictionary * / - (NSDictionary *) generateCrashMetaDataFromReport; @end NS_ASSUME_NONNULL_END // HCTTestCase.m #import "HCTTestCase.h" #import ... @implementation HCTTestCase #pragma mark - life cycle - (void)setUp { [super setUp]; Self.net workTimeout = 20.0; // 1. Set platform info [self setupAppProfile]; // 2. Set Mget configuration [[TITrinityInitManager sharedInstance] setup]; / /... // 3. Set HermesClient [[HermesClient sharedInstance] setup]; } - (void)tearDown { [super tearDown]; } #pragma mark - public Method - (void)waitForExpectationsWithCommonTimeout { [self waitForExpectationsWithCommonTimeoutUsingHandler:nil]; } - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler { [self waitForExpectationsWithTimeout:self.networkTimeout handler:handler]; } - (NSDictionary *)generateCrashMetaDataFromReport { NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary]; NSDate *crashTime = [NSDate date]; metaDictionary[@"MONITOR_TYPE"] = @"appCrash"; / /... metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000); return [metaDictionary copy]; } #pragma mark - private method - (void)setupAppProfile { [[CMAppProfile sharedInstance] setMPlatform:@"70"]; / /... } @endCopy the code
-
The above is basically about development specifications. Inside the test method if you call a method of another class, you must Mock out an external object inside the test method, limiting the return value, and so on.
-
It is difficult to use mocks or stubs within XCTest, which are very common and important features in testing
example
Here is an example to test a database operation class HCTDatabase, the code only put the test code of a method.
- (void)testRemoveLatestRecordsByCount { XCTestExpectation *exception = [self ExpectationWithDescription: @ "delete the latest data function test database"]; / / 1. The first empty tables [dbInstance removeAllLogsInTableType: HCTLogTableTypeMeta]; NSMutableArray *insertModels = [NSMutableArray array]; NSMutableArray *reportIDS = [NSMutableArray array]; for (NSInteger index = 1; index <= 100; index++) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = index; / /... if (index > 90 && index <= 100) { [reportIDS addObject:model.report_id]; } [insertModels addObject:model]; } [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta]; / / 3. The early data deleted (id 90 && id > < = 100) [dbInstance removeLatestRecordsByCount: 10 inTableType: HCTLogTableTypeMeta]; // 4. Get the current top 10 data and compare it with the previous top 10 stored ids. To determine whether the current total number of records in the table is equal to 90 [dbInstance getLatestRecoreds: 10 inTableType: HCTLogTableTypeMeta completion: ^ (NSArray < HCTLogModel *> * _Nonnull records) { NSArray<HCTLogModel *> *latestRTentRecords = records; [dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) { NSArray<HCTLogModel *> *currentRecords = records; __block BOOL isEarlyData = NO; [latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([reportIDS containsObject:obj.report_id]) { isEarlyData = YES; } }]; XCTAssert(! IsEarlyData && currentRecords. Count = = 90 @ "* * * Database latest article n data" delete "function: abnormal"); [exception fulfill];}];}]; [self waitForExpectationsWithCommonTimeout]; }Copy the code
3. Test the framework
1. Kiwi
Kiwi in the BDD framework is commendable. Introduce pod ‘Kiwi’ using CocoaPods. Look at the following example
The Planck project is an SDK based on WebView. According to business scenarios, it is found that most of the function customization for WebView is based on the life cycle of WebView, so reference NodeJS middleware idea. Design WebView middleware based on life cycle.)
#import <Foundation/Foundation.h>
@interface TPKTrustListHelper : NSObject
+(void)fetchRemoteTrustList;
+(BOOL)isHostInTrustlist:(NSString *)scheme;
+(NSArray *)trustList;
@end
Copy the code
The test class
SPEC_BEGIN(TPKTrustListHelperTest)
describe(@"Middleware Wrapper", ^{
context(@"when get trustlist", ^{
it(@"should get a array of string",^{
NSArray *array = [TPKTrustListHelper trustList];
[[array shouldNot] beNil];
NSString *first = [array firstObject];
[[first shouldNot] beNil];
[[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
});
});
context(@"when check a string wether contained in trustlist ", ^{
it(@"first string should contained in trustlist",^{
NSArray *array = [TPKTrustListHelper trustList];
NSString *first = [array firstObject];
[[theValue([TPKTrustListHelper isHostInTrustlist:first]) should] equal:@(YES)];
});
});
});
SPEC_END
Copy the code
The example contains the most basic elements of Kiwi. SPEC_BEGIN and SPEC_END indicate test classes; Describe classes that need to be tested. Context represents a test scenario, i.e. Given->When->Then; “It” means what you want to test, which is When and Then in Given->When->Then. A describe can contain multiple contexts, and a describe can contain multiple IT contexts.
The use of Kiwi is divided into four parts: Specs, Expectations, Mocks and Stubs, and Asynchronous Testing. Click to access the detailed documentation.
It inside the code block is the real test code, using the chain call way, easy to get started.
Mocks and stubs are very important in the testing world. Mock objects reduce dependencies between objects and simulate a clean testing environment (similar to the idea of “control variables” in junior high physics). Kiwi also supports it very well, emulating objects, emulating empty objects, emulating protocol-compliant objects, and so on. ClickMocks and StubsLook at it. Stub stubs can control the return value of a method, which is useful for calling method return values of other objects within a method. Reduce dependence on the outside world and single test whether current behavior meets expectations.
For asynchronous tests, XCTest creates an XCTestExpectation object and calls the KID method of that object in the asynchronous implementation. Finally set the maximum waiting time and complete the callback – (void) waitForExpectationsWithTimeout: NSTimeInterval timeout handler: (nullable XCWaitCompletionHandler)handler; The following example
XCTestExpectation * exception = [self expectationWithDescription: @ "test database insert function"]. [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta]; NSMutableArray *insertModels = [NSMutableArray array]; for (NSInteger index = 1; index <= 10000; index++) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = index; / /... [insertModels addObject:model]; } [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta]; [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) { XCTAssert(count == InsertModels. Count, @"**Database 'add' function: exception "); [exception];}]; [self waitForExpectationsWithCommonTimeout];Copy the code
2. Expecta, Specta
Expecta and Specta are the brainchild of Orta, one of the developers of Cocoapods. This is awesome. Big guy in engineering, quality assurance.
Specta is a lightweight BDD testing framework that uses A DSL pattern to make tests closer to natural language and therefore easier to read.
Features:
- Easy to integrate into projects. Check it in Xcode
Include Unit Tests
, used with XCTest - The syntax is very formal, and comparing Kiwi and Specta’s documentation shows that many things are the same, i.e., very formal, so learning costs are low and migration to other frameworks is smooth.
Expecta is a matching (assertion) framework, and Excepta provides richer assertions than Xcode’s Assertion XCAssert.
Features:
- Eepecta has no data type restrictions, such as 1, and does not care whether it is NSInteger or CGFloat
- Chain programming, very comfortable to write
- Reverse matching, very flexible. Assertion matching
except(...) .to.equal(...)
Is used if the assertion does not match.notTo
or.toNot
- Delay matching can be added after the chain expression
.will
,.willNot
,.after(interval)
等
4. Summary
Xcode comes with XCTestCase, which is suitable for TDD and does not affect the source code, system independence and App package size. Suitable for testing in simple scenarios. And each function in the far left and a test button, click after you can test a function alone.
Kiwi is a powerful BDD framework for slightly more complex projects, comfortable to write, powerful, mock objects, stub syntax, asynchronous testing, and more for almost all testing scenarios. Cannot inherit from XCTest.
Specta is also a BDD framework based on XCTest and can be used with the XCTest template collection. Specta is lighter than Kiwi. In the development of general use with Excepta. Mock and Stud can be paired with OCMock if needed.
Excepta is a matching framework that is a bit more comprehensive than the assertion of XCTest.
There is no way to say which is the best and most reasonable, so choose the right combination according to the requirements of the project.
Five, network test
When we test a method, we may encounter that network communication capability is invoked inside the method. If the network request is successful, the UI may be refreshed or some successful hints are given. If the network fails or is unavailable, failure messages are displayed. Therefore, network communication needs to be simulated.
Many of the networks in iOS are implemented based on classes under the NSURL system. So we can take advantage of NSURLProtocol’s ability to monitor the network and mock the network data. Check out this article if you are interested.
The open source project OHHTTPStubs is a library for network emulation. It can intercept HTTP requests, return JSON data, and customize various headers.
Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!
Several main classes and their capabilities: HTTPStubsProtocol intercepts network requests; The HTTPStubs singleton manages the HTTPStubsDescriptor instance object. HTTPStubsResponse forges an HTTP request.
HTTPStubsProtocol inherits from NSURLProtocol to filter HTTP requests before they are sent
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] ! = nil); if (! found && HTTPStubs.sharedInstance.onStubMissingBlock) { HTTPStubs.sharedInstance.onStubMissingBlock(request); } return found; }Copy the code
FirstStubPassingTestForRequest method can judge whether the request need to be within the current object
Then the network request is sent. In fact, any network capability can be used in the – (void)startLoading method to complete the request, such as NSURLSession, NSURLConnection, AFNetworking, or any other network framework. OHHTTPStubs gets request and client objects. If the HTTPStubs singleton contains an onStubActivationBlock object, the block is executed and an HTTPStubsResponse response object is returned using the responseBlock object.
The API for OHHTTPStubs can be seen in the documentation.
For example, test the offline package functionality with Kiwi and OHHTTPStubs. The following code
@interface HORouterManager (Unittest) - (void)fetchOfflineInfoIfNeeded; @end SPEC_BEGIN(HORouterTests) describe(@"routerTests", ^{ context(@"criticalPath", ^{ __block HORouterManager *routerManager = nil; beforeAll(^{ routerManager = [[HORouterManager alloc] init]; }); it(@"getLocalPath", ^{ __block NSString *pagePath = nil; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ pagePath = [routerManager filePathOfUrl:@"http://***/resource1"]; }); [[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil]; __block NSString *rescPath = nil; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ rescPath = [routerManager filePathOfUrl:@"http://***/resource1"]; }); [[expectFutureValue(rescPath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil]; }); it(@"fetchOffline", ^{ [HOOfflineManager sharedInstance].offlineInfoInterval = 0; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [request.URL.absoluteString containsString:@"h5-offline-pkg"]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"code"] = @(0); dict[@"data"] = @"f722fc3efce547897819e9449d2ac562cee9075adda79ed74829f9d948e2f6d542a92e969e39dfbbd70aa2a7240d6fa3e51156c067e8685402727b 6c13328092ecc0cbc773d95f9e0603b551e9447211b0e3e72648603e3d18e529b128470fa86aeb45d16af967d1a21b3e04361cfc767b7811aec6f19c 274d388ddae4c8c68e857c14122a44c92a455051ae001fa7f2b177704bdebf8a2e3277faf0053460e0ecf178549e034a086470fa3bf287abbdd0f798 67741293860b8a29590d2c2bb72b749402fb53dfcac95a7744ad21fe7b9e188881d1c24047d58c9fa46b3ebf4bc42a1defc50748758b5624c6c439c1 82fe21d4190920197628210160cf279187444bd1cb8707362cc4c3ab7486051af088d7851846bea21b64d4a5c73bd69aafc4bb34eb0862d1525c4f9a 62ce64308289e2ecbc19ea105aa2bf99af6dd5a3ff653bbe7893adbec37b44a088b0b74b80532c720c79b7bb59fda3daf85b34ef35"; NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:@{@"Content-Type":@"application/json"}]; }]; [routerManager fetchOfflineInfoIfNeeded]; [[HOOfflineInfo shouldEventually] receive:@selector(saveToLocal:)]; }); }); }); SPEC_ENDCopy the code
😂, the code I posted has several times to see the combination of different test frameworks, so it is not to say that the selection of framework A is done, according to the scenario to choose the best solution.
6. UI testing
The above article talks a lot about the topics related to unit test. Unit test is very suitable for testing code quality, logic, network and other contents, but it is not suitable for the final product App. It is obviously not suitable for testing the correctness of UI interface and whether the function is correct. UI Testing, launched by Apple in Xcode 7, is Apple’s own UI Testing framework.
The underlying implementation of many UI automation testing frameworks relies on Accessibility, also known as App usability. UI Accessibility is a user-friendly feature introduced in iOS 3.0, which helps people with physical disabilities to easily use apps.
Accessibility classifies and marks UI elements. Categories like buttons, text boxes, text, and so on, using identifiers to distinguish different UI elements. The accessibilityIdentifier is also used to bind business data in the design and implementation of traceless burial points.
- To use Xcode’s built-in UI Tests, select Include UI Tests when creating the project.
- Like the unit test sense, the UI test method name starts with test. Move the mouse cursor inside the method and click the red button at the lower left of Xcode to start recording the UI script.
Explanation:
/ *! Proxy for an application that may or may not be running. */ @interface XCUIApplication : XCUIElement // ... @endCopy the code
-
XCUIApplication Launch to launch the test. XCUIApplication is a proxy for UIApplication in the test process, used for some interaction with the App.
-
Use staticTexts to get a proxy for the UILabel element on the current screen. Equivalent to [app descendantsMatchingType: XCUIElementTypeStaticText]. XCUIElementTypeStaticText parameter is enumerated types.
typedef NS_ENUM(NSUInteger, XCUIElementType) { XCUIElementTypeAny = 0, XCUIElementTypeOther = 1, XCUIElementTypeApplication = 2, XCUIElementTypeGroup = 3, XCUIElementTypeWindow = 4, XCUIElementTypeSheet = 5, XCUIElementTypeDrawer = 6, XCUIElementTypeAlert = 7, XCUIElementTypeDialog = 8, XCUIElementTypeButton = 9, XCUIElementTypeRadioButton = 10, XCUIElementTypeRadioGroup = 11, XCUIElementTypeCheckBox = 12, XCUIElementTypeDisclosureTriangle = 13, XCUIElementTypePopUpButton = 14, XCUIElementTypeComboBox = 15, XCUIElementTypeMenuButton = 16, XCUIElementTypeToolbarButton = 17, XCUIElementTypePopover = 18, XCUIElementTypeKeyboard = 19, XCUIElementTypeKey = 20, XCUIElementTypeNavigationBar = 21, XCUIElementTypeTabBar = 22, XCUIElementTypeTabGroup = 23, XCUIElementTypeToolbar = 24, XCUIElementTypeStatusBar = 25, XCUIElementTypeTable = 26, XCUIElementTypeTableRow = 27, XCUIElementTypeTableColumn = 28, XCUIElementTypeOutline = 29, XCUIElementTypeOutlineRow = 30, XCUIElementTypeBrowser = 31, XCUIElementTypeCollectionView = 32, XCUIElementTypeSlider = 33, XCUIElementTypePageIndicator = 34, XCUIElementTypeProgressIndicator = 35, XCUIElementTypeActivityIndicator = 36, XCUIElementTypeSegmentedControl = 37, XCUIElementTypePicker = 38, XCUIElementTypePickerWheel = 39, XCUIElementTypeSwitch = 40, XCUIElementTypeToggle = 41, XCUIElementTypeLink = 42, XCUIElementTypeImage = 43, XCUIElementTypeIcon = 44, XCUIElementTypeSearchField = 45, XCUIElementTypeScrollView = 46, XCUIElementTypeScrollBar = 47, XCUIElementTypeStaticText = 48, XCUIElementTypeTextField = 49, XCUIElementTypeSecureTextField = 50, XCUIElementTypeDatePicker = 51, XCUIElementTypeTextView = 52, XCUIElementTypeMenu = 53, XCUIElementTypeMenuItem = 54, XCUIElementTypeMenuBar = 55, XCUIElementTypeMenuBarItem = 56, XCUIElementTypeMap = 57, XCUIElementTypeWebView = 58, XCUIElementTypeIncrementArrow = 59, XCUIElementTypeDecrementArrow = 60, XCUIElementTypeTimeline = 61, XCUIElementTypeRatingIndicator = 62, XCUIElementTypeValueIndicator = 63, XCUIElementTypeSplitGroup = 64, XCUIElementTypeSplitter = 65, XCUIElementTypeRelevanceIndicator = 66, XCUIElementTypeColorWell = 67, XCUIElementTypeHelpTag = 68, XCUIElementTypeMatte = 69, XCUIElementTypeDockItem = 70, XCUIElementTypeRuler = 71, XCUIElementTypeRulerMarker = 72, XCUIElementTypeGrid = 73, XCUIElementTypeLevelIndicator = 74, XCUIElementTypeCell = 75, XCUIElementTypeLayoutArea = 76, XCUIElementTypeLayoutItem = 77, XCUIElementTypeHandle = 78, XCUIElementTypeStepper = 79, XCUIElementTypeTab = 80, XCUIElementTypeTouchBar = 81, XCUIElementTypeStatusItem = 82, }; Copy the code
-
The descendantsMatchingType: method is called with the XCUIApplication instantiation object to get the XCUIElementQuery type. For example, @Property (readonly, copy*) XCUIElementQuery *staticTexts;
/ *! Returns a query for all descendants of the element matching the specified type. */ - (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;Copy the code
-
DescendantsMatchingType Returns type matches for all descendents. ChildrenMatchingType Returns the type match object for the current hierarchy’s children
/ *! Returns a query for direct children of the element matching the specified type. */ - (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;Copy the code
-
You cannot get XCUIElement directly after you get XCUIElementQuery. Like XCUIApplication, XCUIElement does not have direct access to UI elements; it acts as a proxy for UI elements in the test framework. Accessibility can be obtained through frame and identifier in Accessibility.
Compare many automated testing frameworks to find UI elements, namely identifiers for Accessibility. The unique identifier here generates a comparison to the exploration of adding automated test tags for UIAutomation]
There are a lot of third-party UI automation testing frameworks, you can check the typical Appium, MacACA.
7. Summary of test experience
TDD writes the test before writing the business code, BDD writes the implementation code before writing the behavior-based test code. Another way to think about it is that there is no need to test for each class’s private methods or for each method, because when all functionality is done, interface testing for each class will generally cover most methods. If the method is not overridden, add Unit Test accordingly.
At present, UI testing (APpium) is still recommended to be done when the core logic has not been changed for a long time, so that each release can be regarded as the core logic regression. At present, the value is convenient for subsequent iterations and maintenance. The rest of the functional testing is still BDD.
For classes, functions, methods to TDD, honestly write UT, UT coverage control.
UITesting is still recommended with no changes to the core logic for a long time, so it can be considered as a core logic regression for every release, so far the value is more convenient for subsequent iterations and maintenance. For example, after the user center SDK was upgraded, there was UITesing, which basically eliminated the involvement of testers.
If it is some active page and logic changes frequently, honestly go test the black box…
I think there’s always been a misconception that automated testing is about quality, but quality comes with it, and testing first makes development faster and better
The WWDC chart also makes it clear that UI needs to be driven by a single test.
The resources
- Wikipedia: Test-driven development