12/08/2018, 15:12

Unit Testing và XCTest trong iOS

Unit Test là gì? Là phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình. Trong lập trình thủ tục, một unit có thể là cả chương trình, một function hay một procedure. Còn trong lập trình hướng ...

Unit Test là gì?

  • Là phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình.
  • Trong lập trình thủ tục, một unit có thể là cả chương trình, một function hay một procedure. Còn trong lập trình hướng đối tượng, đơn vị nhỏ nhất có lẽ là một method của một class nào đó.

Lợi ích của Unit Test

  • Tạo ra môi trường lý tưởng để kiểm tra bất kỳ đoạn mã nào, có khả năng thăm dò và phát hiện lỗi chính xác, duy trì sự ổn định của toàn bộ phần mềm và giúp tiết kiệm thời gian so với công việc gỡ rối truyền thống.
  • Phát hiện các thuật toán thực thi không hiệu quả, các thủ tục chạy vượt quá giới hạn thời gian.
  • Phát hiện các vấn đề về thiết kế, xử lý hệ thống, thậm chí các mô hình thiết kế.
  • Phát hiện các lỗi nghiêm trọng có thể xảy ra trong những tình huống rất hẹp.
  • Tạo hàng rào an toàn cho các khối mã: Bất kỳ sự thay đổi nào cũng có thể tác động đến hàng rào này và thông báo những nguy hiểm tiềm tàng. Unit Test tạo thành hàng rào an toàn cho mã ứng dụng
  • Unit Test là môi trường lý tưởng để tiếp cận các thư viện API bên ngoài một cách tốt nhất. Sẽ rất nguy hiểm nếu chúng ta ứng dụng ngay các thư viện này mà không kiểm tra kỹ lưỡng công dụng của các thủ tục trong thư viện. Dành ra thời gian viết Unit Test kiểm tra từng thủ tục là phương pháp tốt nhất để khẳng định sự hiểu đúng đắn về cách sử dụng thư viện đó. Ngoài ra, Unit Test cũng được sử dụng để phát hiện sự khác biệt giữa phiên bản mới và phiên bản cũ của cùng một thư viện.

Trong môi trường làm việc cạnh tranh, UT còn có tác dụng rất lớn đến năng suất làm việc:

  • Giải phóng chuyên viên QA khỏi các công việc kiểm tra phức tạp.
  • Tăng sự tự tin khi hoàn thành một công việc. Chúng ta thường có cảm giác không chắc chắn về các đoạn mã của mình như liệu các lỗi có quay lại không, hoạt động của module hiện hành có bị tác động không, hoặc liệu công việc hiệu chỉnh mã có gây hư hỏng đâu đó...
  • Là công cụ đánh giá năng lực của bạn. Số lượng các tình huống kiểm tra (test case) chuyển trạng thái "pass" sẽ thể hiện tốc độ làm việc, năng suất của bạn.

XCTest là gì?

  • XCTest là thư viện kiểm thử (Unit testing framework) của Apple để lập trình viên kiểm thử lại ứng dụng của mình.

  • XCTest được tích hợp sẵn từ Xcode 5 trở đi.

  • Phiên bản và khả năng tương thích: Trong Xcode5, XCTest tương thích với OS X v10.8, v10.9, và với iOS 7.x. Trong Xcode 6.x, XCTest tương thích với OS X v10.9, v10.10, và với iOS 6.x.

  • Nếu đã sử dụng OCUnit test trên Xcode trước đây, chúng ta có thể dễ dàng nhận ra sự tương đồng của OCUnit và XCTest. XCTest là một sự cải tiến của OCUnit, tương thích hơn với Xcode giúp dễ dàng cho việc kiểm thử.

  • Việc thực hiện kiểm thử sử dụng XCTest có thể được lặp đi lặp lại để chắc chắn rằng ứng dụng không có lỗi, hoặc các đoạn code mới không phá vỡ cấu trúc, chức năng hiện có.

  • Mặc định thì mọi project Xcode được tạo mới sẽ tự động có một lớp chứa các phương thức test mẫu.

Trong phần còn lại của tài liệu này, chúng ta sẽ tìm hiểu cách create, run, debug test bằng cách sử dụng các công cụ hỗ trợ sẵn trong Xcode.

Tạo một Unit Test case đầu tiên trên Xcode

Bước 1: Tạo các test case class

Tạo một Single Application project có tên "XCTestTutorial". Xcode sẽ tự động tạo một thư mục có tên "XCTestTutorialTests" chứa file "XCTestTutorialTests.m" với một số phương thức mẫu được generate sẵn. Để tạo một Test Case class một cách thủ công, ta có thể làm như sau:Chuột phải vào XCTestTutorial chọn New File. Chọn Test Case Class trong iOS->Source. Đặt tên cho TestCase với hậu tố Tests: Chúng ta có lập nhiều class Test để nhóm các Test Case tuỳ theo chức năng của chúng. Ở ví dụ này có hai Class là NumberTestStringTest. Kết quả sẽ như sau:

Bước 2: Xoá bỏ các phương thức mẫu không cần thiết:

Như ta đã thấy, trong file Tests Case vừa tạo sẽ có 4 phương thức được tự sinh là setUp(), tearDown(), testExample(), testPerformanceExample(). Xóa bỏ 2 phương thức testExample()testPerformanceExample() vì chúng ta sẽ viết lại chúng từ đầu. Hai phương thức setUp(), tearDown() là hai phương thức ghi đè từ lớp cha XCTestCase.

Bước 3: Import và cài đặt dữ liệu Test

Chúng ta có hai class là NumberString dùng để xử lý các dữ liệu có kiểu number và string. Trước tiên ta tiến hành Import hai file .h và tạo property có kiểu NumberString bên trong class Test Case vừa tạo.

@property (nonatomic) Number *numberTest;

Trong phương thức setup(), ta khởi tạo thuộc tính như sau:

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.numberTest = [[Number alloc]init];
}

Bước 4: Bổ sung phương thức private

Sẽ không có vấn đề gì khi ta gọi đến các phương thức được khai báo trong file .h của class NumberString. Tuy nhiên, đối với các phương thức được khai báo trong file .m, chúng ta sẽ không thể gọi đến các phương thức này thông qua property vừa được khai báo. Để gọi đến các phương thức này, thay vì thêm một khai báo của nó trong file .h, ta có thể sử dụng một cách khác là tạo một Private Category. Category là mở rộng trực tiếp một lớp không cần kế thừa. (Đọc thêm tại http://rypress.com/tutorials/objective-c/categories) Private Category là một category được khai báo bên trong một lớp, ta chỉ có thể sử dụng category trong lớp ấy. Bên trong class Test Case tạo ở Bước 1, ta khai báo một Private Category như sau:

@interface Number (Test)

-(BOOL)isPrimeNumber:(NSInteger) inputNumer;

@end	

Bước 5: Viết Test case:

Trong class Number có hai phương thức có kiểu BOOLisPrimeNumber()isSquareNumber() dùng để kiểm tra số nguyên tố và số chính phương. Để kiểm tra xem hai phương thức này có hoạt động chính xác không, ta viết các phương thức test cho chúng trong file "NumberTests.m". Cụ thể hơn, để test phương thức isPrimeNumber() ta viết một phương thức testIsPrimeNumber(), trong đó tạo một đối tượng NSString và truyền vào phương thức isPrimaNumber().

-(void)testIsPrimeNumber {
    NSInteger thisIsPrimeNumber = 11;
    [self.numberTest isPrimeNumber:thisIsPrimeNumber];
}

Tại đây ta vẫn chưa thu được kết quả gì cho việc test, vì chúng ta chưa test phương thức isPrimeNumber() xem nó có cho kết quả đúng khi truyền vào một số nguyên tố hay không? Trong XCTest Framework hỗ trợ rất nhiều hàm khác nhau để hỗ trợ cho việc test, ví dụ như so sánh các loại dữ liệu trả về với kết quả mong muốn hay kiểm tra giá trị kiểu BOOL. Trong trường hợp này là giá trị trả về kiểu BOOL. Ta sẽ sử dụng function là XCTAssertTrue() để test phương thứ isPrimeNumber(). Vì dữ liệu ban đầu truyền vào là số 11 - một số nguyên tố,nên nếu dữ liệu trả về của isPrimeNumber()True thì Test Case ở trạng thái Success, còn ngược lại là Fail.

-(void)testIsPrimeNumber {
    NSInteger thisIsPrimeNumber = 11;
    XCTAssertTrue([self.numberTest isPrimeNumber:thisIsPrimeNumber], @""Fail"");
}
  • Trên đây là một ví dụ về cách viết Test cho một phương thức có kiểu trả về là BOOL. Sau đây ta xét một ví dụ về cách Test một phương thức có kiểu trả về là một Object: Trong class String, phương thức reverseString() dùng để đảo ngược một xâu NSString truyền vào, dữ liệu trả về có kiểu NSString. Trong class StringTest, ta viết phương thức testReverseString() dùng để test xem phương thức reverseString() có trả về xâu đảo ngượcmột cách chính xác không như sau:
- (void)testReverseString {
    NSString *originalString = @"himynameisandy";
    NSString *reversedString = [self.testString reverseString:originalString];
    NSString *expectedReversedString = @"ydnasiemanymih";
    XCTAssertEqualObjects(expectedReversedString, reversedString, @"The reversed string did not match the expected reverse");
}

Dữ liệu test gồm có:

  • biến originalString: dữ liệu đầu vào phương thức reverseString().
  • biến expectedReversedString: giá trị kì vọng của sẽ thu được.

Sau khi gọi phương thức reverseString, dữ liệu được lưu và biến reversedString. Bây giờ ta cần so sánh xem xâu thu được này có giống với giá trị mà ta kì vọng hay không. Nếu giống thì quá trình Test sẽ thành công. Hàm XCTAssertEqualObject() dùng để so sánh hai đối tượng bất kì với nhau, nếu hai đồi tượng giống nhau thì test case sẽ Success, ngược lại là Fail.

Các Hàm Test

"Ngoài ra hai hàm sử dụng ở trên, XCTest Framework cũng hỗ trợ nhiều hàm khác cũng để phục vụ cho việc test, thông tin về các hàm này ta có thể tìm thấy trong QuickHelp của Xcode. Các hàm Test được kiệt kê theo thể loại như sau: Các phần sau đây liệt kê các assert (khẳng định) XCTest. Bạn có thể lấy thêm thông tin về những assert XCTest bằng cách tham khảo XCTestAssertions.h trong Xcode sử dụng QuickHelp (Trợ giúp nhanh).

Assertion categories Description
Unconditional Fail XCTFail. Tạo ra một sự kiện Fail vô điều kiện.
XCTFail ((format...)
Equality Tests (Hàm test So sánh) XCTAssertEqualObjects. Tạo ra một sự kiện Fail khi expression1 không bằng expression2 (hoặc một đối tượng là nil còn đối tượng kia khác nil).
XCTAssertEqualObjects (expression1, expression2, format ...)
XCTAssertNotEqualObjects. Tạo ra một sự kiện Fail khi expression1 và expression2 giống nhau.
XCTAssertNotEqualObjects (expression1, expression2, format ...)
XCTAssertEqual. Tạo ra một sự kiện Fail khi expression1 và expression2 không giống nhau.
XCTAssertEqual (expression1, expression2, format ...)
XCTAssertNotEqual. Tạo ra một sự kiện Fail khi expression1 bằng expression2.
XCTAssertNotEqual (expression1, expression2, format ...)
XCTAssertEqualWithAccuracy. Tạo ra một sự kiện Fail khi sự khác biệt giữa expression1 và expression2 lớn hơn một độ chính xác nào đó.
XCTAssertEqualWithAccuracy (expression1, expression2, accuracy, format ...)
XCTAssertNotEqualWithAccuracy. Tạo ra một sự kiện Fail khi sự khác biệt giữa expression1 và expression2 là nhỏ hơn hoặc bằng với độ chính xác nào đó.
XCTAssertNotEqualWithAccuracy (expression1, expression2, accuracy, format ...)
XCTAssertGreaterThan. Tạo ra một sự kiện Fail khi expression1 nhỏ hơn hoặc bằng expression2. Test đối các giá trị vô hướng.
XCTAssertGreaterThan (expression1, expression2, format ...)
XCTAssertGreaterThanOrEqual. Tạo ra một sự kiện Fail khi expression1 nhỏ hơn expression2. Test đối các giá trị vô hướng.
XCTAssertGreaterThanOrEqual (expression1, expression2, format ...)
XCTAssertLessThan. Tạo ra một sự kiện Fail khi expression1 lớn hơn hoặc bằng expression2.Test đối các giá trị vô hướng.
XCTAssertLessThan (expression1, expression2, format ...)
XCTAssertLessThanOrEqual. Tạo ra một sự kiện Fail khi expression1 lớn hơn expression2. Test đối các giá trị vô hướng.
XCTAssertLessThanOrEqual (expression1, expression2,format ...)
Nil Tests (Test các giá trị nil) XCTAssertNil. Tạo ra một sự kiện Fail khi tham số truyền vào không phải là nil.
XCTAssertNil (expression, format...)
XCTAssertNotNil. Tạo ra một sự kiện Fail khi tham số biểu hiện là nil.
XCTAssertNotNil (expression, format ...)
Boolean Tests XCTAssertTrue. Tạo ra một sự kiện Fail khi expression có giá trị False (NO).
XCTAssertTrue (expression, format ...)
XCTAssert. Tạo ra một sự kiện Fail khi expression có giá trị False (NO). Giống với XCTAssertTrue.
XCTAssert (expression, format ...)
XCTAssertFalse. Tạo ra một sự kiện Fail khi biểu thức có giá trị True (YES).
XCTAssertFalse (epression, format ...)
Exception Tests (Test các trường hợp Ngoại lệ) XCTAssertThrows. Tạo ra một sự kiện Fail khi expression không ném một ngoại lệ (Exception).
XCTAssertThrows (expression, format ...)
XCTAssertThrowsSpecific. Tạo ra một sự kiện Fail khi expression không ném một exception của một Class cụ thể.
XCTAssertThrowsSpecific (expression, exceptionclass, format ...)
XCTAssertThrowsSpecificNamed. Tạo ra một sự kiện Fail khi expression không ném một ngoại lệ của một Class cụ thể với một tên cụ thể. Hữu ích cho những Framework như AppKit hoặc Foundation mà ném ra một NSException chung chung với những cái tên cụ thể (NSInvalidArgumentException, vv). XCTAssertThrowsSpecificNamed (expression, exceptionclass, exceptionname, format ...)
XCTAssertNoThrow. Tạo ra một sự kiện Fail khi một expression ném ra một ngoại lệ. XCTAssertNoThrow (expression, format ...)
XCTAssertNoThrowSpecific. Tạo ra một sự kiện Fail khi expression ném một ngoại lệ của các Class cụ thể. XCTAssertNoThrowSpecific (expression, exceptionclass, format ...)
XCTAssertNoThrowSpecificNamed. Tạo ra một sự kiện Fail khi expression không ném một ngoại lệ của một Class cụ thể với một tên cụ thể. Hữu ích cho những Framework như AppKit hoặc Foundation mà ném một NSException chung chung với những cái tên cụ thể (NSInvalidArgumentException và vv).
XCTAssertNoThrowSpecificNamed (expression, exceptionclass, exceptionname, format ...)

Bước 6: Chạy Unit Test:

Có hai cách để chạy Unit Test đó là chạy trên Test Navigator hoặc chạy trực tiếp trên Source Editor. 1. Chạy Test trên Test Navigator: Chạy tất cả Test case trong project bằng cách bấm vào button Run trong Test Navigator như sau: Chạy tất cả Test Case trong một class bằng cách bấm vào button Run trong Test Navigator như sau: Chạy từng Test Case trong một class bằng cách bấm vào button Run trong Test Navigator như sau: 2. Sử dụng Source Editor Đưa con trỏ chuột vào đầu mỗi phương thức test (biểu tượng hình thoi) sẽ hiển thị nút Run, bấm vào để chạy từng test case:

Hiển thị kết quả Test

Sau khi chạy, ở đầu các hàm ở trạng thái Pass sẽ có một dấu tích màu xanh, các hàm ở trạng thái Fail sẽ có một dấu x màu đỏ. Kết quả này hiển thị cả ở Test Navigator và Source Editor như sau:

What new in Xcode 6

Xcode 6 đã giới thiệu 2 cách thêm Unit Testing mới trên iOS và OS X là chức năng Testing asynchronousMeasuring performance của những đoạn code cụ thể.

Kiểm thử bất đồng bộ (Asynchronous Testing)

Trước khi ra Xcode 6, không có cách nào tốt để Test bất đồng bộ. Nếu trong hàm Test ta gọi phương thức chứa logic bất đồng bộ (mất một khoảng thời gian để thực hiện) thì bạn sẽ không thể kiểm thử được nó. Quá trình sẽ hoàn thành trước khi các logic bất đồng bộ trong các method được thực hiện. Để lập trình kiểm thử bất đồng bộ, Apple đã giới thiệu API cho phép các lập tình viên có thể định nghĩa một kì vọng mà nó phải được thực hiện xong, trước khi quá trình test kết thúc. Qua trình kiểm thử sẽ được thực hiện theo thứ tự như sau: Định nghĩa Expectation (kì vọng,) chờ nó được hoàn thành, hoàn thành Expectation khi đoạn code bất đồng bộ chạy xong.

Ví dụ như sau:

 (void)testDoSomethingThatTakesSomeTime {
    XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Long method"];
    [self.vcToTest doSomethingThatTakesSomeTimesWithCompletionBlock:^(NSString *result) {
        XCTAssertEqualObjects(@"result", result, @"Result was not correct!");
        [completionExpectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

Trong ví dụ này, chúng ta đang kiểm thử phương thức doSomethingThatTakesSomeTimesWithCompletionBlock. Phương thức này được viết trong lớp String, sẽ mất 8 giây để kết thúc. Chúng ta muốn kiểm tra phương thức này có thành công hay không. Để làm việc đó, chúng ta định nghĩa Expectation ở phần đầu của phương thức Test. Ở phần cuối của method, chúng ta chờ cho Expectation thành công với một khoảng thời gian nhất định, ở đây ta sẽ chờ 10s. Khi tiến hành chạy, quá tình kiểm thử sẽ dừng lại "chờ" cho Expectations chạy thành công, hoặc thất bại nếu quá thời gian chờ.

Kiểm thử hiệu suất (Performance Testing)

Còn cách khác để testing trong Xcode 6 là khả năng đo(kiểm tra) hiệu suất của một đoạn mã. Việc này cho phép các lập trình viên có cái nhìn sâu sắc vào các thông tin thời gian cụ thể của đoạn mã đang kiểm thử. Khi kiểm thử hiệu suất, ta có thể biết được thời gian thực hiện trung bình của các đoạn mã cụ thể là bao nhiêu? Ta cũng có thể xác định thời gian thực thi cơ bản, có nghĩa là nếu đoạn mã được kiểm thử chênh lệch so với tiêu chuẩn thì Test case này sẽ không thành công. Xcode sẽ thực thi các đoạn code được Test và đo thời gian thực thi chúng. Để làm được việc này ta sử dụng measureBlock: API như sau:

(void)testPerformanceReverseString {
    NSString *originalString = @"himynameisandy";
    [self measureBlock:^{
        [self.vcToTest reverseString:originalString];
    }];
}

Click trực tiếp để xem, thiết lập và chỉnh sửa thời gian thực thi như sau:

Trên đây tôi đã đề cập đến Unit Testing cũng như cách sử dụng XCTest trên iOS, hi vọng qua đó giúp các bạn có được cái nhìn tổng quát cũng như tầm quan trọng của Unit Test với lập trình nói chung, và XCTest với lập trình iOS nói riêng. Trong bài viết tiếp theo tôi sẽ chia sẻ về cách sử dụng UI Testing, được giới thiệu lần đầu tiên trên Xcode 7, cảm ơn các bạn đã dành thời gian đọc bài viết này.

0