1. Trước khi bắt đầu
Trong các lớp học lập trình trước đây, bạn đã tìm hiểu cách sử dụng Retrofit để tạo yêu cầu mạng. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách viết chương trình kiểm thử đơn vị cho mã mạng của bạn.
Điều kiện tiên quyết
- Bạn đã tạo thư mục kiểm thử trong Android Studio.
- Bạn đã viết mã kiểm thử đơn vị và kiểm thử đo lường trong Android Studio.
- Bạn đã thêm phần phụ thuộc Gradle vào một dự án Android.
Kiến thức bạn sẽ học được
- Cách lưu trữ tài nguyên cho các chương trình kiểm thử.
- Cách mô phỏng phản hồi của API cho việc kiểm thử.
- Cách kiểm thử các dịch vụ Retrofit API.
Bạn cần có
- Một máy tính đã cài đặt Android Studio.
- Mã nguồn giải pháp cho ứng dụng Mars Photos.
Tải mã khởi đầu xuống cho lớp học lập trình này
Trong lớp học lập trình này, bạn sẽ thêm các kiểm thử đo lường vào ứng dụng Mars Photos từ mã nguồn giải pháp trước đó.
Để lấy mã cho lớp học lập trình này và mở trong Android Studio, hãy thực hiện các bước sau.
Lấy mã
- Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
- Trên trang GitHub của dự án, hãy nhấp vào nút Code (Mã), một hộp thoại sẽ xuất hiện.
- Trong hộp thoại này, hãy nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
- Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
- Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.
Mở dự án trong Android Studio
- Khởi động Android Studio.
- Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).
Lưu ý: Nếu Android Studio đã mở sẵn thì thay vào đó, hãy chọn tuỳ chọn sau đây trong trình đơn File > New > Import Project (Tệp > Mới > Nhập dự án).
- Trong hộp thoại Import Project (Nhập dự án), hãy chuyển đến nơi chứa thư mục dự án đã giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
- Nhấp đúp vào thư mục dự án đó.
- Chờ Android Studio mở dự án.
- Nhấp vào nút Run (Chạy) để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
- Duyệt qua các tệp dự án trong cửa sổ công cụ Project (Dự án) để xem cách ứng dụng được thiết lập.
2. Tổng quan về ứng dụng khởi động
Ứng dụng Mars Photos có một màn hình duy nhất, cho thấy danh sách ảnh được truy xuất theo yêu cầu mạng.
Mã khởi đầu này có một số điểm khác biệt liên quan đến việc kiểm thử. Chúng ta không đi sâu vào chi tiết vì những vấn đề này không thuộc phạm vi của lớp học này nhưng sẽ đề cập sơ qua.
Khi kiểm thử cách thức ứng dụng xử lý việc truy xuất dữ liệu qua một API, tốt nhất chúng ta nên tự cung cấp dữ liệu của chính mình để có thể hiểu rõ cấu trúc của dữ liệu đó. Dữ liệu qua API có thể thay đổi. Điều này có thể phá vỡ các kịch bản kiểm thử cũng như gây ra các lỗi kiểm thử không cần thiết. Ngoài ra, việc thực hiện một lệnh gọi mạng thực tế có thể gây ra các lỗi do kết nối hoặc tốc độ mạng. Điều này có thể làm cho kết quả kiểm thử trở nên không nhất quán. Do đó, chúng ta sẽ tạo một số dữ liệu trong các chương trình kiểm thử và lưu trữ dữ liệu đó dưới dạng tệp JSON trong thư mục test/res. Đây là thư mục tài nguyên, tương tự như thư mục tài nguyên trong mã ứng dụng chính, chỉ khác là tài nguyên kiểm thử được lưu trữ trong test/res.
- Thêm thư mục
test/res
. Nhấp chuột phải vào thư mụcsrc
trong chế độ xem dự án rồi chọn New (Mới) -> Directory (Thư mục) trên trình đơn thả xuống.
- Trong cửa sổ bật lên, hãy di chuyển xuống rồi chọn
test/res
.
- Trong chế độ xem dự án, hãy nhấp chuột phải vào thư mục
test/res
rồi chọn New (Mới) -> File (Tệp).
- Đặt tên
mars_photos.json
cho tệp.
- Sao chép mã nguồn sau đây vào tệp
mars_photos.json
.
[
{
"id":"424905",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"
},
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
}
]
- Để có thể truy cập các tệp trong thư mục tài nguyên này, bạn cần chỉ định rõ thư mục tài nguyên kiểm thử làm thư mục "nguồn" trong tệp bản dựng. Hãy thêm dòng lệnh sau đây:
app/build.gradle
android {
...
sourceSets {
test.resources.srcDirs += 'src/test/res'
}
}
Việc này cho phép chúng ta truy cập vào các tệp tài nguyên mà không cần nhập đầy đủ đường dẫn đến tệp đó trong mã nguồn khi sử dụng những tài nguyên này trong quá trình kiểm thử. Nhập đường dẫn đầy đủ là một cách kiểm thử kém hiệu quả vì đường dẫn tệp có thể thay đổi theo máy và hệ điều hành.
- Cũng trong tệp đó, hãy thêm các phần phụ thuộc rồi đồng bộ hoá Gradle như dưới đây.
app/build.gradle
dependencies {
...
testImplementation 'junit:junit:4.12'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
}
- Bây giờ, hãy tạo một thư mục kiểm thử thông thường.
- Trong thư mục kiểm thử này, hãy tạo một gói mới
com.example.android.marsphotos
. - Trong gói này, hãy tạo một tệp Kotlin mới có tên
BaseTest.kt
.
- Sao chép và dán mã nguồn sau đây vào tệp
BaseTest.kt
. Dù bạn không thấy quen thuộc với mã nguồn này thì cũng không sao! Thuật ngữ "Mock" ("Mô phỏng") xuất hiện liên tục trong mã nguồn dưới đây và là chủ đề chúng ta sẽ thảo luận trong lớp học lập trình này. Hàmenqueue()
trong lớp này sẽ phân tích cú pháp dữ liệu trong tệpmars_photos.json
mà bạn đã tạo, nhờ vậy sau này bạn có thể dùng lại tệp này để viết chương trình kiểm thử khác.
BaseTest.kt
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
open class BaseTest {
val mockWebServer = MockWebServer()
fun enqueue(fileName: String) {
val inputStream = javaClass.classLoader!!.getResourceAsStream(fileName)
val buffer = inputStream.source().buffer()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(buffer.readString(Charsets.UTF_8))
)
}
}
3. Dữ liệu mô phỏng
Trong lớp học lập trình này, chủ yếu chúng ta sẽ dùng dữ liệu mô phỏng. Khi nói đến kiểm thử, mô phỏng là việc giả lập giá trị trả về của một đoạn mã. Trong một lớp học lập trình trước đây, chúng ta đã mô phỏng một lớp. Chúng ta cũng có thể mô phỏng các hàm và yêu cầu các hàm này trả về các giá trị theo chỉ định hoặc mô phỏng một API để trả về một phần dữ liệu theo chỉ định. Việc này hữu ích cho quá trình kiểm thử vì tính năng này giúp chúng ta tách riêng mã nguồn phục vụ cho việc kiểm thử. Trong lớp học lập trình này, chúng ta sẽ tập trung vào việc mô phỏng phản hồi API. Chủ đề về các hàm mô phỏng sẽ được đề cập trong một lớp học lập trình khác.
4. Tạo lớp kiểm thử đơn vị
Trong kiểm thử này, chúng ta sẽ kiểm thử dịch vụ API. Trước tiên, hãy tạo một lớp mới có tên là MarsApiServiceTests.kt.
5. Phần phụ thuộc
Mã khởi đầu cho lớp học lập trình này đã bao gồm các phần phụ thuộc cần thiết. Tuy nhiên, có một phần phụ thuộc quan trọng mà chúng ta chưa nhắc tới.
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
Phần phụ thuộc này cho phép tạo một máy chủ mô phỏng. Về cơ bản, máy chủ mô phỏng sẽ chặn các yêu cầu mạng và định tuyến lại các yêu cầu này để trả về dữ liệu mô phỏng theo đúng yêu cầu. Chúng ta sẽ thảo luận về ý nghĩa của việc mô phỏng này ở các phần sau trong lớp học lập trình này.
6. Các phương pháp hay nhất
Hãy lưu ý rằng chúng ta đang viết chương trình kiểm thử bằng ngôn ngữ lập trình hướng đối tượng Kotlin. Tức là chúng ta có thể viết mã nguồn hướng đối tượng cho chương trình kiểm thử. Với lượng mã sẽ viết cho chương trình kiểm thử này, thực ra bạn không cần sử dụng phương pháp hướng đối tượng. Tuy nhiên, để minh hoạ về mặt khái niệm, chúng ta sẽ triển khai mã nguồn theo phương pháp này.
Bạn có thể nhận thấy mã khởi đầu đã bao gồm thư mục kiểm thử đơn vị, trong đó có một lớp tên là BaseTest
. Nếu có một đoạn mã được dùng cho nhiều kiểm thử, chúng ta có thể tạo một open class
và các lớp kiểm thử của chúng ta sẽ kế thừa từ đó. Hãy lưu ý rằng lớp BaseTest này chỉ có một phương thức tên là enqueue()
và chúng ta chỉ viết một lớp kiểm thử trong lớp học lập trình này. Khi viết tự viết chương trình kiểm thử của mình, bạn có thể sử dụng phương pháp lập trình hướng đối tượng để khai thác những lợi ích của phương pháp này. Tuy nhiên, bạn không nên lạm dụng phương pháp này nếu không cần thiết.
7. Viết chương trình kiểm thử yêu cầu mạng
Trước tiên, hãy đảm bảo rằng lớp kiểm thử của bạn được kế thừa từ BaseTest
.
Chúng ta sẽ trực tiếp kiểm thử dịch vụ MarsApiService
, vì vậy chúng ta sẽ cần một thực thể của lớp dịch vụ này. Chúng ta sẽ thiết lập lớp này giống như cách bạn đã làm trong mã nguồn ứng dụng, nhưng với một địa chỉ URL khác của dịch vụ mới này.
- Trong lớp kiểm thử, hãy tạo biến
lateinit
choMarsApiService
.
private lateinit var service: MarsApiService
Bây giờ, chúng ta cần một hàm định nghĩa biến service
trước mỗi lần kiểm thử.
- Tạo một hàm có tên là
setup()
rồi chú thích hàm đó bằng@Before
.
@Before
fun setup() {}
Đối với chương trình kiểm thử này, chúng tôi sẽ không sử dụng URL cho các yêu cầu mạng. Đây là lúc MockWebServer
phát huy tác dụng.
Dữ liệu mô phỏng
Trong lớp BaseTest
, có một thuộc tính tên là mockWebServer,
. Thuộc tính này đơn thuần là một thực thể của đối tượng MockWebServer
. Đối tượng này sẽ giao cắt với các yêu cầu mạng của chúng ta, nhưng trước tiên chúng ta cần chuyển hướng các yêu cầu mạng này đến URL sẽ được giao cắt.
MockWebServer
có một hàm tên là url()
, dùng để xác định URL mà chúng ta muốn giao cắt. Hãy nhớ rằng chúng ta không muốn tạo một yêu cầu mạng có thực mà chỉ muốn tạo một yêu cầu giả để có thể kiểm thử mã mạng bằng chính dữ liệu mà chúng ta kiểm soát trong kiểm thử. Hàm url()
nhận một chuỗi đại diện cho URL giả mạo đó rồi trả về một đối tượng HttpUrl
. Trong hàm setup()
, hãy viết dòng lệnh sau đây:
val url = mockWebServer.url("/")
Chúng ta đã thiết lập điểm cuối URL mà chúng ta muốn chặn và đã nhận được đối tượng trả về HttpUrl
.
Bên trong hàm này, hãy tạo một thực thể của MarsApiService
theo cách tương tự đã thực hiện với MarsApiService
và MarsApiClass
(ngoại trừ biến lazy
). Tuy nhiên, hàm này vẫn chưa có một URL cơ sở (base URL). Hàm này có dạng như sau:
service = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
Bây giờ, chúng ta sẽ thiết lập URL cơ sở. Thêm đoạn mã sau đây vào chuỗi hàm: Retrofit.Builder()
:
.baseUrl(url)
Việc này báo cho dịch vụ API biết rằng chúng ta muốn định tuyến các yêu cầu đến MockWebServer
.
service = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
Xin nhắc lại, điểm mấu chốt của MockWebServer
là để tránh gửi một yêu cầu mạng thực đến một API thực. Về cơ bản, khi thực hiện một yêu cầu mạng thực sự thì quá trình kiểm thử sẽ không thành công nếu API này không thành công. Khi sử dụng API thực, API này sẽ tự kiểm thử chính nó còn chúng ta chỉ quan tâm đến việc kiểm thử mã nguồn dự án Android của mình.
Bạn có thể xem MockWebServer
là một API giả trả về dữ liệu đã tạo. Do đó, chúng ta cần cho MockWebServer
biết rõ dữ liệu nào cần được trả về trước khi tạo yêu cầu. Đây là nơi chứa hàm enqueue()
trong BaseTest
. Đừng quá lo lắng về mã nguồn trong hàm này vì phần mã nguồn này nằm ngoài phạm vi lớp học lập trình này. Chúng ta chỉ cần biết rằng hàm này lấy một tệp trong tài nguyên kiểm thử rồi biến tệp đó thành một phản hồi API giả mạo.
- Tạo một hàm kiểm thử tên là
api_service()
. Trong hàm kiểm thử này, hãy gọi phương thứcenqueue
như sau:
enqueue("mars_photos.json")
- Tiếp theo, chúng ta trực tiếp gọi hàm
getPhotos()
quaMarsApiService
. Nên nhớ rằnggetPhotos()
là một hàm tạm ngưng (suspend function) và phải được gọi trong phạm vi coroutine. Chúng ta thực hiện điều này bằng cách gói lệnh gọi phương thức này trongrunBlocking
như dưới đây:
runBlocking {
val apiResponse = service.getPhotos()
}
- Bây giờ, hãy đảm bảo kết quả phản hồi của
getPhotos()
khácnull
. Hãy nhớ rằng chúng ta đã xác định biếnapiResponse
bên trongrunBlocking
, vì vậy, chúng ta cũng phải truy cập biến này bên trongrunBlocking
.
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
}
getPhotos()
trả về danh sách chứa các đối tượng MarsPhoto
, vì vậy hãy đảm bảo danh sách trả về có số lượng phần tử như dự kiến.
- Xem tệp mars_photos.json trong test/res. Tạo một lệnh xác nhận khác để đảm bảo danh sách này khác rỗng. Đồng thời, hãy tạo một lệnh xác nhận để kiểm tra xem một số dữ liệu đã chính xác hay chưa. Chuyển đến test/res/mars_photos.json rồi sao chép một trong các mã nhận dạng. Xác nhận rằng giá trị của mã nhận dạng đó tương đương với giá trị của mã nhận dạng trong mục danh sách tương ứng.
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
assertTrue("The list was empty", apiResponse.isNotEmpty())
assertEquals("The IDs did not match", "424905", apiResponse[0].id)
}
Mã nguồn kiểm thử bây giờ sẽ có dạng như sau:
8. Mã nguồn giải pháp
9. Xin chúc mừng
Hoạt động kiểm thử yêu cầu mạng có thể rất phức tạp và lớp học lập trình này chỉ đề cập đến những khái niệm tổng quan về chủ đề này. Mọi chương trình kiểm thử mạng bạn viết sẽ đều là duy nhất đối với các API mà ứng dụng nhận dữ liệu qua đó. Khi đã có thêm kinh nghiệm xử lý API, bạn có thể mở rộng chương trình kiểm thử để mô phỏng nhiều loại lỗi mạng cũng như phản hồi API. Đây có thể là một dạng kiểm thử không đơn giản. Để thành thạo dạng kiểm thử này, bạn phải trải qua nhiều lần thử và sai. Vì vậy, hãy cố gắng luyện tập không ngừng nhé.
Trong lớp học lập trình này, chúng ta đã tìm hiểu:
- Cách áp dụng phương pháp lập trình hướng đối tượng trong hoạt động kiểm thử.
- Cách lưu trữ tài nguyên trong thư mục kiểm thử.
- Cách gọi hàm tạm ngưng trong một chương trình kiểm thử
- Cách mô phỏng phản hồi API trong một lượt kiểm thử.