📜 ⬆️ ⬇️

IOS development testing

Content:




Development through testing - what is it?


Development through testing (Test-driven development) is a software development technique that defines development through writing tests. In essence, you need to perform three simple repeating steps:
- Write a test for the new functionality that needs to be added;
- Write the code that will pass the test;
- To refactor new and old code.

Martin fowler

')



Three laws TDD


Now, many people know that TDD requires us to write tests first, before writing production code. But this rule is only the tip of the iceberg. I suggest you consider the following three laws:
  1. Production code is not written before there is an idle test for it;
  2. No more unit test code is written than enough for its error. And non-compilability is a mistake;
  3. No more production code is written than enough to pass the current non-working test.

These three laws keep the programmer in a cycle that lasts about a minute. Tests and production code are written simultaneously, with tests just a few seconds ahead of the production code.
If we work this way, we will write dozens of tests a day, hundreds every month, thousands every year. If we work this way, then our tests will fully cover our production code.

Application examples


Example 1. Sorting model objects

Consider the simplest example of writing a method for sorting model objects (events). First, we write the interface of the new method (it takes a chaotic array of objects as input, and returns accordingly sorted):

Test Method Interface:

- (NSArray *)sortEvents:(NSArray *)events 

Next, write the test:
We use the given, when, then notation to divide the test into three logical blocks:

Test:

  - (void)testSortEvents { // given id firstEvent = [self mockEventWithClass:kCinemaEventType name:@""]; id secondEvent = [self mockEventWithClass:kCinemaEventType name:@""]; id thirdEvent = [self mockEventWithClass:kPerfomanceEventType name:@""]; id fourthEvent = [self mockEventWithClass:kPerfomanceEventType name:@""]; NSArray *correctSortedArray = @[firstEvent, secondEvent, thirdEvent, fourthEvent]; NSArray *incorrectSortedArray = @[thirdEvent, secondEvent, fourthEvent, firstEvent]; // when NSArray *sortedWithTestingMethodArray = [self.service sortEvents:incorrectSortedArray]; // then XCTAssertEqualObjects(correctSortedArray, sortedWithTestingMethodArray, @"   "); } 

Only after we make sure that the test fails, we write the implementation of the method:

Method implementation:

  - (NSArray *)sortEvents:(NSArray *)events { //   ( ,  ,    ) NSMutableArray *cinemaEvents = [[NSMutableArray alloc] init]; NSMutableArray *otherEvents = [[NSMutableArray alloc] init]; for (Event *event in events) { if ([event.type isEqualToString:kCinemaEventType]) { [cinemaEvents addObject:event]; } else { [otherEvents addObject:event]; } } NSComparisonResult (^sortBlock)(Event *firstEvent, Event *secondEvent) = ^NSComparisonResult(Event *firstEvent, Event *secondEvent) { return [firstEvent.type compare:secondEvent.type options:NSNumericSearch]; }; [cinemaEvents sortUsingComparator:sortBlock]; [otherEvents sortUsingComparator:sortBlock]; return [cinemaEvents arrayByAddingObjectsFromArray:otherEvents]; } 

After writing the implementation of the method, we check that all tests, including our new one, are successfully executed. If necessary, we refactor the written code. It is important to understand that refactoring can be carried out only from a green state to a green one, i.e. when all tests are successfully completed.

A very important feature of TDD is the inverse of responsibility. In the classical approach to software development, the programmer, after writing the code, conducts the testing himself, drives the code in the debugger, and checks the operation of all if-s. In TDD, the tests confirm the performance of the code.



Example 2. Mapper

Consider the second example - the mapper server output in the model objects.

Mapper must meet the following requirements:
1) map objects (unexpectedly)
2) handle the output curve (Null, wrong data type, wrong output structure)
3) to ensure the consistency of the data (checking the required fields)

We write the interface of the test method, it will be synchronous:

Test Method Interface:

  - (NSArray *)mapEvents:(id)responseObject 

Next, we write the first test, verifying the positive scenario, i.e. mapping objects with adequate issuance. We do not write all the tests at once, do not try to cover all the requirements that the mapper must meet, and write a single test for the simplest implementation:

Test:

  - (void)testMapEventsResponseSuccessful { // given NSDictionary *responseObject = @{@"event" : @[@{@"type" : @1, @"name" : @"test", @"description" : @"test"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; [self checkObject:event forKeyExistence:@[@"type", @"name", @"description"]]; } 

To check the object for the presence of the required fields, use the helper method on KVC:

  - (void)checkObject:(id)object forKeyExistence:(NSArray *)keys { for (NSString *key in keys) { if (![object valueForKey:key]) { XCTFail(@"     - %@", key); } } } 

Next, we write the simplest implementation of the method that meets our test and verify that the test will be successfully passed. In this case, the mapper uses a RESTKit mapping engine under the hood, but we consider our method as a black box, its implementation may change in the future. But the requirements in most cases will remain that way.

Method implementation:

  - (NSArray *)mapEvents:(id)responseObject { NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]}; RKMappingResult *mappingResult = [self mapResponseObject:responseObject withMappingsDictionary:mappingsDictionary]; if (mappingResult) { return [mappingResult array]; } return @[]; } 

Well, after writing the simplest implementation, we add another test for the mapper, which checks that the mapper takes into account the mandatory nature of some fields in the output, in our case it will be the id field, which is not in the test. The test task is to make sure that there is no crash, and the method will return an empty array after the mapping.

Test:

  - (void)testMapEventsResponseWithMissingMandatoryFields { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"name" : @"test"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then XCTAssertFalse([events count]); } 

Further we expand our method, we add in it check on obligatory some fields.

Method implementation:

  - (NSArray *)mapEvents:(id)responseObject { NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]}; RKMappingResult *mappingResult = [self mapResponseObject:responseObject withMappingsDictionary:mappingsDictionary]; if (mappingResult) { //      NSArray *events = [self checkMandatoryFields:[mappingResult array]]; return events; } return @[]; } 

Based on the initial requirements, our mapper should behave correctly if there are null s in the output, we write a test that checks:

  - (void)testMapEventsResponseWithNulls { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @1, @"name" : [NSNull null], @"description" : [NSNull null]}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; XCTAssertNotNil(event.type, @""); XCTAssertNil(event.name, @""); } 

After writing and running the test, we see that it runs immediately immediately. Yes, this also happens, the existing code may already meet the new requirements. In this situation, we, of course, do not remove this test, because his task in the future, with a possible change in the implementation of the mapper, is to check the correctness of his work. Next, add a test for the wrong data type, instead of a number - a string. Mapping RestKit engine is able to cope with this, so the test will again turn green.

  - (void)testMapEventsResponseWithWrongType { // given NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @"123"}]}; // when NSArray *events = [self.mapper mapEvents:responseObject]; // then Event *event = events[0]; XCTAssertTrue([event.type isEqual:@123]); } 


Example 3. Event Receive Service

Consider a service for obtaining model objects (events), the protocol will contain the following methods:

  @interface EventService : NSObject - (instancetype)initWithClient:(id<Client>)client mapper:(id<Mapper>)mapper; - (NSArray *)obtainEventsForType:(EventType)eventType; - (void)updateEventsForType:(EventType)eventType success:(SuccessBlock)success failure:(ErrorBlock)failure; @end 

The service will work with CoreData, so for testing we will need to initialize the CoreData stack in memory. We use the MagicalRecord library for working with CoreData, do not forget to call [MagicalRecord cleanUp] in teardown. A network client and mapper are injected into our service, so we pass mocks to the initializer.

  @interface EventServiceTests : XCTestCase @property (nonatomic, strong) EventService *eventService; @property (nonatomic, strong) id<Client> clientMock; @property (nonatomic, strong) id<Mapper> mapperMock; @end @implementation EventServiceTests - (void)setUp { [super setUp]; [MagicalRecord setDefaultModelFromClass:[self class]]; [MagicalRecord setupCoreDataStackWithInMemoryStore]; self.clientMock = OCMProtocolMock(@protocol(Client)); self.mapperMock = OCMProtocolMock(@protocol(Mapper)); self.eventService = [[EventService alloc] initWithClient:self.clientMock mapper:self.mapperMock]; } - (void)tearDown { self.eventService = nil; self.clientMock = nil; self.mapperMock = nil; [MagicalRecord cleanUp]; [super tearDown]; } 

Add a test for the method of obtain - synchronous retrieval of objects from the database:

  - (void)testObtainEventsForType { // given Event *event = [Event MR_createEntity]; event.eventType = EventTypeCinema; // when NSArray *events = [self.eventService obtainEventsForType:EventTypeCinema]; // then XCTAssertEqualObjects(event, [events firstObject]); } 

Implementation of the method of obtaining and service initializer:

  - (instancetype)initWithClient:(id<Client>)client mapper:(id<Mapper>)mapper { self = [super init]; if (self) { _client = client; _mapper = mapper; } return self; } - (NSArray *)obtainEventsForType:(EventType)eventType { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"eventType = %d", eventType]; NSArray *events = [Event MR_findAllWithPredicate:predicate]; return events; } 

We add a test for the update method - it queries the server for events using the client, then mappits the model and saves it to the database. We stabilize the client so that it always returns the success block. We check that our service will contact the mapper and call the success block:

  - (void)testUpdateEventsForTypeSuccessful { // given XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"]; OCMStub([self.clientMock requestEventsForType:EventTypeCinema success:OCMOCK_ANY failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) { SuccessBlock block; [invocation getArgument:&block atIndex:3]; block(); }); // when [self.eventService updateEventsForType:EventTypeCinema success:^{ [expectation fulfill]; } failure:^(NSError *error) { }]; // then [self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil]; OCMVerify([self.mapperMock mapEvents:OCMOCK_ANY forType:EventTypeCinema mappingContext:OCMOCK_ANY]); } 

We write a test on the script of the update method error, we are stopping the client so that it returns an error:

  - (void)testUpdateEventsForTypeFailure { // given XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"]; NSError *clientError = [NSError errorWithDomain:@"" code:1 userInfo:nil]; OCMStub([self.clientMock requestEventsForType:EventTypeCinema success:OCMOCK_ANY failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) { ErrorBlock block; [invocation getArgument:&block atIndex:4]; block(error); }); // when [self.eventService updateEventsForType:EventTypeCinema success:^{ } failure:^(NSError *error) { XCTAssertEqual(clientError, error) [expectation fulfill]; }]; // then [self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil]; } 

Implementation of the update method:

  - (void)updateEventsForType:(EventType)eventType success:(SuccessBlock)success failure:(ErrorBlock)failure { @weakify(self); [self.client requestEventsForType:eventType success:^(id responseObject) { @strongify(self); NSManagedObjectContext *context = [NSManagedObjectContext MR_rootSavingContext]; NSArray *events = [self.mapper mapEvents:responseObject forType:eventType mappingContext:context]; [context MR_saveToPersistentStoreAndWait]; success(); } failure:^(NSError *error) { failure(error); }]; } 

Advantages and disadvantages


Benefits



disadvantages





Development through testing encourages simple design and inspires confidence.
TDD encourages simple designs and inspires confidence.

Kent Beck

Literature and links


Kent Beck - Test-Driven Development: By example
Robert C. Martin - Clean code
www.objc.io/issue-15
en.wikipedia.org/wiki/Test-driven_development
qualitycoding.org/objective-c-tdd
agiledata.org/essays/tdd.html#WhatIsTDD
www.basilv.com/psd/blog/2009/test-driven-development-benefits-limitations-and-techniques
martinfowler.com/articles/is-tdd-dead
habrahabr.ru/post/206828
habrahabr.ru/post/216923
iosunittesting.com

Source: https://habr.com/ru/post/263087/


All Articles