- Khi tham khảo các
topic
vềunit testing
hayautomated test
ta thường bắt gặp các thuật ngữ rất phổ biến như vềtestable code
nhưsynchronous
,predicable
hay luôn trả raoutput
giống nhau cho cácinput
khác nhau. Tuy nhiên trong thực tế các đoạn code vềnet working
mà chúng ta hay gặp lại đi ngược với các thuật ngữ chúng ta gặp trong các topic. - Nguyên nhân khiến chúng ta gặp khó khăn khi test code
net working
lànet working
vốn dĩ là một công việc không đồng bộ và phụ thuộc nhiều vào các yếu tố bên ngoài như làinternet connection
,server
, các loại hệ thống vận hành khác nhau. Các yếu tố trên đều ảnh hưởng trực tiếp đến việcperforming
,loading
,decoding
củanet work
request
. - Ở bài viết này chúng ta sẽ tập trung vào cách
test
các đoạn codeasynchronous
bằng cách sử dụngAPI
Foundation
:
1/ Verifying request generation logic:
- Khi bắt đầu triển khai một công việc
test
mới chúng ta nên bắt đầu ngược bằng cách kiểm tra độ chính xác của cáclogic
cơ bản nhất trước khi chuyển sangtest
cácAPI
. - Chúng ta sẽ cùng khơi tạo các
URLRequest
từEndpoint
với một số điều kiện kèm theo .Để việc test có tiêu chuẩn chung chúng ta cùng tạoEndpointKind
mà không hề tùy chỉnh:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">extension</span> <span class="token builtin">EndpointKinds</span> <span class="token punctuation">{</span> <span class="token keyword">enum</span> <span class="token builtin">Stub</span><span class="token punctuation">:</span> <span class="token builtin">EndpointKind</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">func</span> <span class="token function">prepare</span><span class="token punctuation">(</span><span class="token number">_</span> request<span class="token punctuation">:</span> <span class="token keyword">inout</span> <span class="token builtin">URLRequest</span><span class="token punctuation">,</span> with data<span class="token punctuation">:</span> <span class="token builtin">Void</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// No-op</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Chúng ta đã có thể viết các
suite test
với đoạncode
bên trên trong đó chúng ra sẽ cần xác minh chính xác xácURLRequest
cho cácendpoint
cơ bản không cầnHTTP header
.
1 2 3 4 5 6 7 8 | <span class="token keyword">class</span> <span class="token class-name">EndpointTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">testBasicRequestGeneration</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> endpoint <span class="token operator">=</span> <span class="token builtin">Endpoint</span><span class="token operator"><</span><span class="token builtin">EndpointKinds</span><span class="token punctuation">.</span><span class="token builtin">Stub</span><span class="token punctuation">,</span> <span class="token builtin">String</span><span class="token operator">></span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token string">"path"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> request <span class="token operator">=</span> endpoint<span class="token punctuation">.</span><span class="token function">makeRequest</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span>request<span class="token operator">?</span><span class="token punctuation">.</span>url<span class="token punctuation">,</span> <span class="token function">URL</span><span class="token punctuation">(</span>string<span class="token punctuation">:</span> <span class="token string">"https://api.myapp.com/path"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Chúng ta sẽ cần cải thiện thêm
test case
bên trên , chúng ta có thể nhận được kết quả sai vì cả trường hợpURLRequest
chúng ta khởi tạo cũng nhưURL
cho sẵn đều có thể bằngnil
(đây là điều rất khó xảy ra nhưng khi viếttest case
chúng ta không nên bỏ sót bất kì trường hợp nào không chắc chắn). - Tiếp theo chúng ta đang giả định rằng
host
sẽ luôn làapi.myapp.com
, điều này đi được lại với thực tế là cácapp
hiện tại đều hỗ trợ chúng ta sử dụng nhiềunet working enviroment
nhưstaging
hayproduction
, hơn thế nữa là cácapp
còn có một vàiserver
với cáchost
cóaddress
khác nhau. - Chúng ta sẽ gặp vấn đề đầu tiên khi sử dụng
XCTUnwrap
để kiểm tra cácrequest
được khởi tạo không bằngnil
. Chúng ta nên tạotypealias
cho cácEndpoint
đặc biệt:
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">class</span> <span class="token class-name">EndpointTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token keyword">typealias</span> <span class="token builtin">StubbedEndpoint</span> <span class="token operator">=</span> <span class="token builtin">Endpoint</span><span class="token operator"><</span><span class="token builtin">EndpointKinds</span><span class="token punctuation">.</span><span class="token builtin">Stub</span><span class="token punctuation">,</span> <span class="token builtin">String</span><span class="token operator">></span> <span class="token keyword">func</span> <span class="token function">testBasicRequestGeneration</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> endpoint <span class="token operator">=</span> <span class="token function">StubbedEndpoint</span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token string">"path"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> request <span class="token operator">=</span> <span class="token keyword">try</span> <span class="token function">XCTUnwrap</span><span class="token punctuation">(</span>endpoint<span class="token punctuation">.</span><span class="token function">makeRequest</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">,</span> <span class="token function">URL</span><span class="token punctuation">(</span>string<span class="token punctuation">:</span> <span class="token string">"https://api.myapp.com/path"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Để có thể kiểm soát chính xác
host
hoạt động thế nào chúng ta cùng sử dụngURLHost
chuyên dụng để có thể bao bọc một chuỗiString
đơn gian:
1 2 3 4 | <span class="token keyword">struct</span> <span class="token builtin">URLHost</span><span class="token punctuation">:</span> <span class="token builtin">RawRepresentable</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> rawValue<span class="token punctuation">:</span> <span class="token builtin">String</span> <span class="token punctuation">}</span> |
- Lợi ích của việc sử dụng
URLHost
với cáctype
riêng biệt là chúng ta có thể đóng gói các biến thể phổ biến bằng cách sử dụng cácproperty
tĩnh trongenum
như cách tạoproperty
chohost
nhưstaging
vàproduction
cũng nhưdefault
để tự động xử lý cho các trường hợpapp
chạy trongDEBUG
mode
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">extension</span> <span class="token builtin">URLHost</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">var</span> staging<span class="token punctuation">:</span> <span class="token keyword">Self</span> <span class="token punctuation">{</span> <span class="token function">URLHost</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"staging.api.myapp.com"</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">static</span> <span class="token keyword">var</span> production<span class="token punctuation">:</span> <span class="token keyword">Self</span> <span class="token punctuation">{</span> <span class="token function">URLHost</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"api.myapp.com"</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">static</span> <span class="token keyword">var</span> `<span class="token keyword">default</span>`<span class="token punctuation">:</span> <span class="token keyword">Self</span> <span class="token punctuation">{</span> #<span class="token keyword">if</span> <span class="token constant">DEBUG</span> <span class="token keyword">return</span> staging #<span class="token keyword">else</span> <span class="token keyword">return</span> production #endif <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Chúng ta cần
update
thêmEndpoint
cácmethod
khởi tạoURLRequest
bằng cáchenabling
URLHost
L:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token keyword">extension</span> <span class="token builtin">Endpoint</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">makeRequest</span><span class="token punctuation">(</span>with data<span class="token punctuation">:</span> <span class="token builtin">Kind</span><span class="token punctuation">.</span><span class="token builtin">RequestData</span><span class="token punctuation">,</span> host<span class="token punctuation">:</span> <span class="token builtin">URLHost</span> <span class="token operator">=</span> <span class="token punctuation">.</span><span class="token keyword">default</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">URLRequest</span><span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> components <span class="token operator">=</span> <span class="token function">URLComponents</span><span class="token punctuation">(</span><span class="token punctuation">)</span> components<span class="token punctuation">.</span>scheme <span class="token operator">=</span> <span class="token string">"https"</span> components<span class="token punctuation">.</span>host <span class="token operator">=</span> host<span class="token punctuation">.</span>rawValue components<span class="token punctuation">.</span>path <span class="token operator">=</span> <span class="token string">"/"</span> <span class="token operator">+</span> path components<span class="token punctuation">.</span>queryItems <span class="token operator">=</span> queryItems<span class="token punctuation">.</span><span class="token builtin">isEmpty</span> <span class="token operator">?</span> <span class="token constant">nil</span> <span class="token punctuation">:</span> queryItems <span class="token keyword">guard</span> <span class="token keyword">let</span> url <span class="token operator">=</span> components<span class="token punctuation">.</span>url <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token constant">nil</span> <span class="token punctuation">}</span> <span class="token keyword">var</span> request <span class="token operator">=</span> <span class="token function">URLRequest</span><span class="token punctuation">(</span>url<span class="token punctuation">:</span> url<span class="token punctuation">)</span> <span class="token builtin">Kind</span><span class="token punctuation">.</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token operator">&</span>request<span class="token punctuation">,</span> with<span class="token punctuation">:</span> data<span class="token punctuation">)</span> <span class="token keyword">return</span> request <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Để tránh việc phải xử lý thủ công cho mỗi
URL
chúng ta sử dụng chúng ta cần cải thiệnURLHost
để sễ dàng khởi tạo cácURL
đặc biệt với cácpath
như sau:
1 2 3 4 5 6 7 | <span class="token keyword">extension</span> <span class="token builtin">URLHost</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">expectedURL</span><span class="token punctuation">(</span>withPath path<span class="token punctuation">:</span> <span class="token builtin">String</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token constant">URL</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> url <span class="token operator">=</span> <span class="token function">URL</span><span class="token punctuation">(</span>string<span class="token punctuation">:</span> <span class="token string">"https://"</span> <span class="token operator">+</span> rawValue <span class="token operator">+</span> <span class="token string">"/"</span> <span class="token operator">+</span> path<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token keyword">try</span> <span class="token function">XCTUnwrap</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Đoạn
code
trên không chỉ giúp việctest
của chúng ta trở nên chặt chẽ, chính xác cũng như linh động hơn. Chúng ta bây giờ hoàn toàn có thể tạo cáctest case
dễ đọc và dễ kiểm thử hơn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token keyword">class</span> <span class="token class-name">EndpointTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token keyword">typealias</span> <span class="token builtin">StubbedEndpoint</span> <span class="token operator">=</span> <span class="token builtin">Endpoint</span><span class="token operator"><</span><span class="token builtin">EndpointKinds</span><span class="token punctuation">.</span><span class="token builtin">Stub</span><span class="token punctuation">,</span> <span class="token builtin">String</span><span class="token operator">></span> <span class="token keyword">let</span> host <span class="token operator">=</span> <span class="token function">URLHost</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"test"</span><span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">testBasicRequestGeneration</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> endpoint <span class="token operator">=</span> <span class="token function">StubbedEndpoint</span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token string">"path"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> request <span class="token operator">=</span> endpoint<span class="token punctuation">.</span><span class="token function">makeRequest</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> host<span class="token punctuation">:</span> host<span class="token punctuation">)</span> <span class="token keyword">try</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span> request<span class="token operator">?</span><span class="token punctuation">.</span>url<span class="token punctuation">,</span> host<span class="token punctuation">.</span><span class="token function">expectedURL</span><span class="token punctuation">(</span>withPath<span class="token punctuation">:</span> <span class="token string">"path"</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- CHúng ta đã tốn kha khá
effort
để viết các tùy chỉnh cho các API để cải thiện chocode base
chúng ta có thể mạnh mẽ cũng như linh hoạt hơn. Chúng ta có thể tạo thêm nhiềutest case
để kiểm tra các loạiEndpoint
một cách nhanh chóng.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ... func testGeneratingRequestWithQueryItems() throws { let endpoint = StubbedEndpoint(path: "path", queryItems: [ URLQueryItem(name: "a", value: "1"), URLQueryItem(name: "b", value: "2") ]) let request = endpoint.makeRequest(with: (), host: host) try XCTAssertEqual( request?.url, host.expectedURL(withPath: "path?a=1&b=2") ) } } |
- Chúng ta nên tạo thêm một số
test case
kiêm tra cácendpoint
thực để đảm bảocode test
hoạt động đúng. Trong trường hợp chúng ta gặpendpoint
yêu cầu sử dụngauthentication
thì chúng ta sẽ cần thêmheader
Authorization
khi khởi tạorequest
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token keyword">class</span> <span class="token class-name">EndpointTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">func</span> <span class="token function">testAddingAccessTokenToPrivateEndpoint</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> endpoint <span class="token operator">=</span> <span class="token builtin">Endpoint</span><span class="token punctuation">.</span><span class="token function">search</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token string">"query"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> token <span class="token operator">=</span> <span class="token function">AccessToken</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"12345"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> request <span class="token operator">=</span> endpoint<span class="token punctuation">.</span><span class="token function">makeRequest</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> token<span class="token punctuation">,</span> host<span class="token punctuation">:</span> host<span class="token punctuation">)</span> <span class="token keyword">try</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span> request<span class="token operator">?</span><span class="token punctuation">.</span>url<span class="token punctuation">,</span> host<span class="token punctuation">.</span><span class="token function">expectedURL</span><span class="token punctuation">(</span>withPath<span class="token punctuation">:</span> <span class="token string">"search?q=query"</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span>request<span class="token operator">?</span><span class="token punctuation">.</span>allHTTPHeaderFields<span class="token punctuation">,</span> <span class="token punctuation">[</span> <span class="token string">"Authorization"</span><span class="token punctuation">:</span> <span class="token string">"Bearer 12345"</span> <span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
2/ Using integration tests:
- Chúng ta hiện đang sử dụng
URLSession
API
trongFoundation
kết hợp với một vàioperator
củaCombine
để xây dựng corenet working
code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">extension</span> <span class="token builtin">URLSession</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> publisher<span class="token operator"><</span>K<span class="token punctuation">,</span> R<span class="token operator">></span><span class="token punctuation">(</span> <span class="token keyword">for</span> endpoint<span class="token punctuation">:</span> <span class="token builtin">Endpoint</span><span class="token operator"><</span>K<span class="token punctuation">,</span> R<span class="token operator">></span><span class="token punctuation">,</span> using requestData<span class="token punctuation">:</span> K<span class="token punctuation">.</span><span class="token builtin">RequestData</span><span class="token punctuation">,</span> decoder<span class="token punctuation">:</span> <span class="token builtin">JSONDecoder</span> <span class="token operator">=</span> <span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">AnyPublisher</span><span class="token operator"><</span>R<span class="token punctuation">,</span> <span class="token builtin">Error</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">guard</span> <span class="token keyword">let</span> request <span class="token operator">=</span> endpoint<span class="token punctuation">.</span><span class="token function">makeRequest</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> requestData<span class="token punctuation">)</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">Fail</span><span class="token punctuation">(</span> error<span class="token punctuation">:</span> <span class="token function">InvalidEndpointError</span><span class="token punctuation">(</span>endpoint<span class="token punctuation">:</span> endpoint<span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">eraseToAnyPublisher</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token function">dataTaskPublisher</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> request<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">.</span>data<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">decode</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token builtin">NetworkResponse</span><span class="token operator"><</span>R<span class="token operator">></span><span class="token punctuation">.</span><span class="token keyword">self</span><span class="token punctuation">,</span> decoder<span class="token punctuation">:</span> decoder<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">.</span>result<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">eraseToAnyPublisher</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Kết quả
successfully
của đoạncode
trên không phải là kết quả chúng ta nên quá quan tâm vì chúng ta sử dụng một loạt cácAPI
của hệ thống đã được tùy chỉnh, kiểm soát theo một số cách thức đặc biệt khiến các kết quảtest
đạt được thường bị thu hẹp và dễ đoán biết. Protocol
là một phương pháp cải tiện kết quảtest
không chỉ trong trường hợp trên mà còn cho nhiều trường hợp khác mà chúng ta không phải tham gia tùy chỉnh, kiểm soát cácAPI
. Đây là một phương pháp hữu dụng và phổ biến nên ở bài viết này chúng ta sẽ cùng ưu tiên cho cách thức khác:- Trong
Swift
ta thấyURLSession
sử dụngURLProtcol
để tiến hành các tác vụnetwork
, hệ thốngApple
đã cung cấp cũng như hỗ trợ hoàn chỉnh để chúng ta có thể tùy chỉnh bằng việc sử dụng cácclass
. Điều đó có nghĩa là chúng ta có thể tự tùy biến một hệ thốngHTTP Net working stack
mà không phải sửa đổi cácoperator Combine
tùy biến trước đó. - Nhược điểm của
URLProtcol
từ quan điểm cá nhân tôi là nó chủ yếu dựa vào cácstatic method
đồng nghĩa chúng ta sẽ phảiimplement
cácmock
của chúng ta riêng biệt. Cách khắc phục tạm thời là sử dụng thêm protocolMockURLResponser
để cho phép chúng ta tạo cácmock server
trả vềData
hoặcError
cần thiết:
1 2 3 4 | <span class="token keyword">protocol</span> <span class="token builtin">MockURLResponder</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">func</span> <span class="token function">respond</span><span class="token punctuation">(</span>to request<span class="token punctuation">:</span> <span class="token builtin">URLRequest</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Data</span> <span class="token punctuation">}</span> |
- Điều tiếp theo chúng ta cần triển khai là tùy chỉnh thêm
URLProtcol
với việcoverride
cácmethod
như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <span class="token keyword">class</span> <span class="token class-name">MockURLProtocol</span><span class="token operator"><</span><span class="token builtin">Responder</span><span class="token punctuation">:</span> <span class="token builtin">MockURLResponder</span><span class="token operator">></span><span class="token punctuation">:</span> <span class="token builtin">URLProtocol</span> <span class="token punctuation">{</span> <span class="token keyword">override</span> <span class="token keyword">class</span> <span class="token class-name">func</span> <span class="token function">canInit</span><span class="token punctuation">(</span>with request<span class="token punctuation">:</span> <span class="token builtin">URLRequest</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Bool</span> <span class="token punctuation">{</span> <span class="token boolean">true</span> <span class="token punctuation">}</span> <span class="token keyword">override</span> <span class="token keyword">class</span> <span class="token class-name">func</span> <span class="token function">canonicalRequest</span><span class="token punctuation">(</span><span class="token keyword">for</span> request<span class="token punctuation">:</span> <span class="token builtin">URLRequest</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">URLRequest</span> <span class="token punctuation">{</span> request <span class="token punctuation">}</span> <span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function">startLoading</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">guard</span> <span class="token keyword">let</span> client <span class="token operator">=</span> client <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">}</span> <span class="token keyword">do</span> <span class="token punctuation">{</span> <span class="token comment">// Here we try to get data from our responder type, and</span> <span class="token comment">// we then send that data, as well as a HTTP response,</span> <span class="token comment">// to our client. If any of those operations fail,</span> <span class="token comment">// we send an error instead:</span> <span class="token keyword">let</span> data <span class="token operator">=</span> <span class="token keyword">try</span> <span class="token builtin">Responder</span><span class="token punctuation">.</span><span class="token function">respond</span><span class="token punctuation">(</span>to<span class="token punctuation">:</span> request<span class="token punctuation">)</span> <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">try</span> <span class="token function">XCTUnwrap</span><span class="token punctuation">(</span><span class="token function">HTTPURLResponse</span><span class="token punctuation">(</span> url<span class="token punctuation">:</span> <span class="token function">XCTUnwrap</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">,</span> statusCode<span class="token punctuation">:</span> <span class="token number">200</span><span class="token punctuation">,</span> httpVersion<span class="token punctuation">:</span> <span class="token string">"HTTP/1.1"</span><span class="token punctuation">,</span> headerFields<span class="token punctuation">:</span> <span class="token constant">nil</span> <span class="token punctuation">)</span><span class="token punctuation">)</span> client<span class="token punctuation">.</span><span class="token function">urlProtocol</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">,</span> didReceive<span class="token punctuation">:</span> response<span class="token punctuation">,</span> cacheStoragePolicy<span class="token punctuation">:</span> <span class="token punctuation">.</span>notAllowed <span class="token punctuation">)</span> client<span class="token punctuation">.</span><span class="token function">urlProtocol</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">,</span> didLoad<span class="token punctuation">:</span> data<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span> client<span class="token punctuation">.</span><span class="token function">urlProtocol</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">,</span> didFailWithError<span class="token punctuation">:</span> error<span class="token punctuation">)</span> <span class="token punctuation">}</span> client<span class="token punctuation">.</span><span class="token function">urlProtocolDidFinishLoading</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function">stopLoading</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Required method, implement as a no-op.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Chúng ta cần
URLSession
sử dụng cácmock protocol
hơn là sử dụng hệ thốngHTTP
mặc định. Để thực hiện điều đó chúng ta chỉ cần choURLSession
sử dụngMockURLProtocol
nhưng đừng quên khai báo khởi tạo cho chínhURLProtcol
:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">extension</span> <span class="token builtin">URLSession</span> <span class="token punctuation">{</span> <span class="token keyword">convenience</span> <span class="token keyword">init</span><span class="token operator"><</span>T<span class="token punctuation">:</span> <span class="token builtin">MockURLResponder</span><span class="token operator">></span><span class="token punctuation">(</span>mockResponder<span class="token punctuation">:</span> T<span class="token punctuation">.</span><span class="token keyword">Type</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> config <span class="token operator">=</span> <span class="token builtin">URLSessionConfiguration</span><span class="token punctuation">.</span>ephemeral config<span class="token punctuation">.</span>protocolClasses <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">MockURLProtocol</span><span class="token operator"><</span>T<span class="token operator">></span><span class="token punctuation">.</span><span class="token keyword">self</span><span class="token punctuation">]</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>configuration<span class="token punctuation">:</span> config<span class="token punctuation">)</span> <span class="token builtin">URLProtocol</span><span class="token punctuation">.</span><span class="token function">registerClass</span><span class="token punctuation">(</span><span class="token builtin">MockURLProtocol</span><span class="token operator"><</span>T<span class="token operator">></span><span class="token punctuation">.</span><span class="token keyword">self</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
- Với những đoạn
code
trên chúng ta giờ đã có thể cố địnhMockURLResponder
protocol
, để ví dụ chúng ta sẽ tham khảo việcencode
item
sau:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">extension</span> <span class="token builtin">Item</span> <span class="token punctuation">{</span> <span class="token keyword">enum</span> <span class="token builtin">MockDataURLResponder</span><span class="token punctuation">:</span> <span class="token builtin">MockURLResponder</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">let</span> item <span class="token operator">=</span> <span class="token function">Item</span><span class="token punctuation">(</span>title<span class="token punctuation">:</span> <span class="token string">"Title"</span><span class="token punctuation">,</span> description<span class="token punctuation">:</span> <span class="token string">"Description"</span><span class="token punctuation">)</span> <span class="token keyword">static</span> <span class="token keyword">func</span> <span class="token function">respond</span><span class="token punctuation">(</span>to request<span class="token punctuation">:</span> <span class="token builtin">URLRequest</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Data</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token function">NetworkResponse</span><span class="token punctuation">(</span>result<span class="token punctuation">:</span> item<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token keyword">try</span> <span class="token function">JSONEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Chúng ta nên cân nhắc việc sử dụng chế đố
synchronous
màCombine
giới thiệu với việc sử dụngXCtTest
để xây dựng các logicsynchronous
hơn là sử dụngGrand Central Dispatch semaphore
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <span class="token keyword">extension</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> awaitCompletion<span class="token operator"><</span>T<span class="token punctuation">:</span> <span class="token builtin">Publisher</span><span class="token operator">></span><span class="token punctuation">(</span> of publisher<span class="token punctuation">:</span> T<span class="token punctuation">,</span> timeout<span class="token punctuation">:</span> <span class="token builtin">TimeInterval</span> <span class="token operator">=</span> <span class="token number">10</span> <span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">[</span>T<span class="token punctuation">.</span><span class="token builtin">Output</span><span class="token punctuation">]</span> <span class="token punctuation">{</span> <span class="token comment">// An expectation lets us await the result of an asynchronous</span> <span class="token comment">// operation in a synchronous manner:</span> <span class="token keyword">let</span> expectation <span class="token operator">=</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">expectation</span><span class="token punctuation">(</span> description<span class="token punctuation">:</span> <span class="token string">"Awaiting publisher completion"</span> <span class="token punctuation">)</span> <span class="token keyword">var</span> completion<span class="token punctuation">:</span> <span class="token builtin">Subscribers</span><span class="token punctuation">.</span><span class="token builtin">Completion</span><span class="token operator"><</span>T<span class="token punctuation">.</span><span class="token builtin">Failure</span><span class="token operator">></span><span class="token operator">?</span> <span class="token keyword">var</span> output <span class="token operator">=</span> <span class="token punctuation">[</span>T<span class="token punctuation">.</span><span class="token builtin">Output</span><span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">let</span> cancellable <span class="token operator">=</span> publisher<span class="token punctuation">.</span>sink <span class="token punctuation">{</span> completion <span class="token operator">=</span> $<span class="token number">0</span> expectation<span class="token punctuation">.</span><span class="token function">fulfill</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> receiveValue<span class="token punctuation">:</span> <span class="token punctuation">{</span> output<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>$<span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token comment">// Our test execution will stop at this point until our</span> <span class="token comment">// expectation has been fulfilled, or until the given timeout</span> <span class="token comment">// interval has elapsed:</span> <span class="token function">waitForExpectations</span><span class="token punctuation">(</span>timeout<span class="token punctuation">:</span> timeout<span class="token punctuation">)</span> <span class="token keyword">switch</span> completion <span class="token punctuation">{</span> <span class="token keyword">case</span> <span class="token punctuation">.</span><span class="token function">failure</span><span class="token punctuation">(</span><span class="token keyword">let</span> error<span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token keyword">throw</span> error <span class="token keyword">case</span> <span class="token punctuation">.</span>finished<span class="token punctuation">:</span> <span class="token keyword">return</span> output <span class="token keyword">case</span> <span class="token constant">nil</span><span class="token punctuation">:</span> <span class="token comment">// If we enter this code path, then our test has</span> <span class="token comment">// already been marked as failing, since our</span> <span class="token comment">// expectation was never fullfilled.</span> cancellable<span class="token punctuation">.</span><span class="token function">cancel</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- Giống công việc
test
Endpoint
của hệ thống chúng ta đã sử dụng nhiềueffort
để xây dựng các công cụ hữu hiệu để có thể triển khai việctest
với các dòng code ngắn gọn mạch lạc hơn. Chúng ta sẽ cùng tiến hànhtest
công việcsuccessfully load
cũng nhưdecode
củarequest
:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">NetworkIntegrationTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">testSuccessfullyPerformingRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> session <span class="token operator">=</span> <span class="token function">URLSession</span><span class="token punctuation">(</span>mockResponder<span class="token punctuation">:</span> <span class="token builtin">Item</span><span class="token punctuation">.</span><span class="token builtin">MockDataURLResponder</span><span class="token punctuation">.</span><span class="token keyword">self</span><span class="token punctuation">)</span> <span class="token keyword">let</span> accessToken <span class="token operator">=</span> <span class="token function">AccessToken</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"12345"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> publisher <span class="token operator">=</span> session<span class="token punctuation">.</span><span class="token function">publisher</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>latestItem<span class="token punctuation">,</span> using<span class="token punctuation">:</span> accessToken<span class="token punctuation">)</span> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token keyword">try</span> <span class="token function">awaitCompletion</span><span class="token punctuation">(</span>of<span class="token punctuation">:</span> publisher<span class="token punctuation">)</span> <span class="token function">XCTAssertEqual</span><span class="token punctuation">(</span>result<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token builtin">Item</span><span class="token punctuation">.</span><span class="token builtin">MockDataURLResponder</span><span class="token punctuation">.</span>item<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- YEAHH!!!, Điều cuối cùng chúng ta cần quan tâm là chúng ta có thể xác nhận lại các phương thức
networking
hoạt động đúng mong đợi khi xảy ra các lỗi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">enum</span> <span class="token builtin">MockErrorURLResponder</span><span class="token punctuation">:</span> <span class="token builtin">MockURLResponder</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">func</span> <span class="token function">respond</span><span class="token punctuation">(</span>to request<span class="token punctuation">:</span> <span class="token builtin">URLRequest</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Data</span> <span class="token punctuation">{</span> <span class="token keyword">throw</span> <span class="token function">URLError</span><span class="token punctuation">(</span><span class="token punctuation">.</span>badServerResponse<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">NetworkIntegrationTests</span><span class="token punctuation">:</span> <span class="token builtin">XCTestCase</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">func</span> <span class="token function">testFailingWhenEncounteringError</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> session <span class="token operator">=</span> <span class="token function">URLSession</span><span class="token punctuation">(</span>mockResponder<span class="token punctuation">:</span> <span class="token builtin">MockErrorURLResponder</span><span class="token punctuation">.</span><span class="token keyword">self</span><span class="token punctuation">)</span> <span class="token keyword">let</span> accessToken <span class="token operator">=</span> <span class="token function">AccessToken</span><span class="token punctuation">(</span>rawValue<span class="token punctuation">:</span> <span class="token string">"12345"</span><span class="token punctuation">)</span> <span class="token keyword">let</span> publisher <span class="token operator">=</span> session<span class="token punctuation">.</span><span class="token function">publisher</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>latestItem<span class="token punctuation">,</span> using<span class="token punctuation">:</span> accessToken<span class="token punctuation">)</span> <span class="token function">XCTAssertThrowsError</span><span class="token punctuation">(</span><span class="token keyword">try</span> <span class="token function">awaitCompletion</span><span class="token punctuation">(</span>of<span class="token punctuation">:</span> publisher<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |