1. Mở đầu
Do dòng đời đưa đẩy và khách thì tiền ít nhưng lại thích hít hàng thơm (toàn đòi 80~100 coverage -_-) nên mình cũng đã kinh qua react-native-testing-library một thời gian kha khá, và cũng nghịch thử Detox, nhưng chưa thằng nào làm mình hài lòng:
- Testing-library: Khi test e2e thì phải mock quá nhiều, khá khó để mock component sao cho hoạt động giống như component thật. Có những lúc mình chỉ viết để cho % coverage lên chứ các fn của test đó bị mock hết, thành ra chả có ý nghĩa gì nhiều. Các thành phần UI bị mock hết, nên test pass mà vẫn chết là điều bình thường như cân đường hộp sữa.
- Detox: chơi với mỗi simulator, device thật ko rõ tới giờ đã được đánh điện tử chưa.
Và gần đây thì mình đọc lại docs của React Native có recommend thêm 1 thằng – Appium. Appium sử dụng driver như XCUITest, UIAutomator, Espresso, … Được các teito (tay to :v) native ở công ty bảo mấy thằng driver trên toàn là hàng hịn bên native dùng để chạy test, nên thử nghịch luôn. Sau một thời gian mày mò thì khá hài lòng.
Tại sao lại dùng Appium mà ko chạy cơm cho khỏe?
Nói thế chứ thực ra thì chạy cơm không hề khỏe tí nào đâu các bạn.
Khi ta phát triển 1 feature mới cho app, ta phải test cho cả những tính năng đã có, ảnh hưởng tới chúng là điều khó tránh khỏi.
Thông thường dev chúng ta sẽ nghĩ là “Có tester rồi xoắn gì” đúng không nào? Tuy nhiên phận dev quèn thì cũng vẫn phải check qua những case cơ bản, (test thêm case dị càng tốt) trước khi chuyển qua cho tester. Nhưng không lẽ cứ viết một feature là ngồi bật app lên bấm từng nút, chưa kể xe đạp mới ( mình có một người bạn rất hay nhầm newbie và newbike =)) ) newbie không nắm rõ hệ thống, lack case là điều tất nhiên, hơn nữa con người chúng ta không giỏi trong những việc nhàm chán có tính lặp lại cao như test app, vì vậy dù có là bô lão thì cũng sẽ có lúc nhầm lẫn.
Ngoài ra, ở cuốn “The Effective Engineer” của Edmond Lau (nếu ko có gì sai sót thì là trang 159) có viết:
If you have to do something manually more than twice, then write a tool for the third time
Vậy thì sao chúng ta không để máy làm những công việc nhàm chán như test, và focus vào những việc khiến ta hứng thú hơn. Hãy cùng học viết test tự động cho ứng dụng React Native của chúng ta thôi nào!
Một vài kiến thức cần có
- React Native, Babel (không cần quá mát tơ)
- Nắm được cơ bản về viết unit test (với jest, hay gì đó tương tự)
- Typescript (optional): do ở bài viết sẽ dùng Typescript
Hardware
- Máy Mac/Hackintosh để build iOS hoặc máy Win (chỉ chạy được Android thôi)
- 1 con điện thoại iOS hoặc Android – nói không với integration test trên simulator :v
2. Chạy thử ví dụ nhỏ
Tự dưng nhồi một đống lý thuyết suông, trong khi không biết mình sẽ làm gì có phải rất chán không nào? Vì vậy trước tiên ta hãy chạy thử một ví dụ nhỏ sau trước đã: Đăng kí account
Ví dụ sau dùng Typescript + WebdriverIO, và có cấu trúc tương đương với ví dụ trên trang chủ của Appium link (Các bạn cũng có thể vào link trên bằng truy cập trang chủ Appium rồi bấm vào nút Examples), tất nhiên để go pro thì ta sẽ ko code như vậy, mình sẽ thực hiện refactor và chuyển đổi môi trường chạy test ở các bài sau ( nếu có =)) ).
Sau khi setup Babel cùng với Jest (do Jest luôn đi kèm với React Native nên mình dùng luôn để demo cho thân thiện), hãy viết 1 đoạn test đơn giản cho quá trình sign up của ta.
Hiện tại mình chỉ fill những field text input, vì field select, date picker viết khá dài, nên mình sẽ đề cập sau.
Nói nhiều quá, code đâu?
Vâng, code đây (ở dưới có giải thích code)
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | <span class="token keyword">import</span> <span class="token punctuation">{</span> BrowserObject<span class="token punctuation">,</span> remote<span class="token punctuation">,</span> RemoteOptions <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"webdriverio"</span><span class="token punctuation">;</span> <span class="token keyword">const</span> iOS<span class="token operator">:</span> RemoteOptions <span class="token operator">=</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">"/wd/hub"</span><span class="token punctuation">,</span> host<span class="token operator">:</span> <span class="token string">"localhost"</span><span class="token punctuation">,</span> port<span class="token operator">:</span> <span class="token number">4723</span><span class="token punctuation">,</span> capabilities<span class="token operator">:</span> <span class="token punctuation">{</span> platformName<span class="token operator">:</span> <span class="token string">"iOS"</span><span class="token punctuation">,</span> automationName<span class="token operator">:</span> <span class="token string">"XCUITest"</span><span class="token punctuation">,</span> deviceName<span class="token operator">:</span> <span class="token string">"iPhone (2)"</span><span class="token punctuation">,</span> platformVersion<span class="token operator">:</span> <span class="token string">"14.1"</span><span class="token punctuation">,</span> app<span class="token operator">:</span> <span class="token string">"org.reactjs.native.example.LearnRnE2eTest"</span><span class="token punctuation">,</span> udid<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">IOS_DEVICE_UUID</span><span class="token punctuation">,</span> xcodeOrgId<span class="token operator">:</span> <span class="token string">"xxx"</span><span class="token punctuation">,</span> xcodeSigningId<span class="token operator">:</span> <span class="token string">"Apple Development"</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> <span class="token keyword">const</span> android<span class="token operator">:</span> RemoteOptions <span class="token operator">=</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">"/wd/hub"</span><span class="token punctuation">,</span> host<span class="token operator">:</span> <span class="token string">"localhost"</span><span class="token punctuation">,</span> port<span class="token operator">:</span> <span class="token number">4723</span><span class="token punctuation">,</span> capabilities<span class="token operator">:</span> <span class="token punctuation">{</span> automationName<span class="token operator">:</span> <span class="token string">"UiAutomator2"</span><span class="token punctuation">,</span> platformName<span class="token operator">:</span> <span class="token string">"android"</span><span class="token punctuation">,</span> platformVersion<span class="token operator">:</span> <span class="token string">"8.0.0"</span><span class="token punctuation">,</span> deviceName<span class="token operator">:</span> <span class="token string">"BH9057609A"</span><span class="token punctuation">,</span> appPackage<span class="token operator">:</span> <span class="token string">"com.learnrne2etest"</span><span class="token punctuation">,</span> appActivity<span class="token operator">:</span> <span class="token string">".MainActivity"</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> <span class="token keyword">let</span> client<span class="token operator">:</span> BrowserObject<span class="token punctuation">;</span> <span class="token function">beforeEach</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> client <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">remote</span><span class="token punctuation">(</span>iOS<span class="token punctuation">)</span><span class="token punctuation">;</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>client<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">afterEach</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>client<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">deleteSession</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><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token function-variable function">scrollTo</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">a11yId<span class="token operator">:</span> string</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>client<span class="token punctuation">.</span>options<span class="token punctuation">.</span>capabilities<span class="token punctuation">.</span>platformName <span class="token operator">===</span> <span class="token string">"android"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token string">"mobile: scroll"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> strategy<span class="token operator">:</span> <span class="token string">"accessibility id"</span><span class="token punctuation">,</span> selector<span class="token operator">:</span> a11yId<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 punctuation">}</span><span class="token punctuation">;</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">"sign up the user"</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> toRegistrationScreenButton <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~login/toRegistrationScreenButton"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> toRegistrationScreenButton<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> registerButton <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/registerButton"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> <span class="token function">scrollTo</span><span class="token punctuation">(</span><span class="token string">"registration/toLoginScreenButton"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> registerButton<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> <span class="token function">scrollTo</span><span class="token punctuation">(</span><span class="token string">"registration/usernameInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> usernameError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/usernameInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> usernameError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Please enter username"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> passwordError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/passwordInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> passwordError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Please enter password"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">let</span> passwordConfirmationError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/passwordConfirmationInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> passwordConfirmationError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Please confirm your password"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> fullNameError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/fullNameInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> fullNameError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Please enter your full name"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> usernameInput <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/usernameInput"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> usernameInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"user..01"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> usernameError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/usernameInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> usernameError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Username must be alphabet and numbers"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> usernameInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"user"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> passwordInput <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/passwordInput"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> passwordInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"password"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> passwordConfirmationInput <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/passwordConfirmationInput"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> passwordConfirmationInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"password123"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> passwordConfirmationError <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/passwordConfirmationInput-error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> passwordConfirmationError<span class="token punctuation">.</span><span class="token function">getText</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">toMatch</span><span class="token punctuation">(</span><span class="token string">"Password confirmation must match your password"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> passwordConfirmationInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"password"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> fullNameInput <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registration/fullNameInput"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> fullNameInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"Test"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">hideKeyboard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> <span class="token function">scrollTo</span><span class="token punctuation">(</span><span class="token string">"registration/toLoginScreenButton"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> registerButton<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> client<span class="token punctuation">.</span><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"~registrationCompleted/toLoginScreenButton"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> element<span class="token punctuation">.</span><span class="token function">isDisplayed</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> timeout<span class="token operator">:</span> <span class="token number">10000</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><span class="token punctuation">;</span> |
Chi tiết source code có thể xem tại đây
Source code của các màn hình có thể xem tại đây:
Và đây là thành phẩm:
1 2 3 4 5 6 7 8 9 10 11 12 | $ yarn test <span class="token constant">PASS</span> __tests__<span class="token operator">/</span>sign<span class="token operator">-</span>up<span class="token punctuation">.</span>spec<span class="token punctuation">.</span><span class="token function">ts</span> <span class="token punctuation">(</span><span class="token number">18.817</span> s<span class="token punctuation">)</span> ✓ sign up the <span class="token function">user</span> <span class="token punctuation">(</span><span class="token number">17179</span> ms<span class="token punctuation">)</span> Test Suites<span class="token operator">:</span> <span class="token number">1</span> passed<span class="token punctuation">,</span> <span class="token number">1</span> total Tests<span class="token operator">:</span> <span class="token number">1</span> passed<span class="token punctuation">,</span> <span class="token number">1</span> total Snapshots<span class="token operator">:</span> <span class="token number">0</span> total Time<span class="token operator">:</span> <span class="token number">18.869</span> s<span class="token punctuation">,</span> estimated <span class="token number">29</span> s Ran all test suites<span class="token punctuation">.</span> ✨ Done <span class="token keyword">in</span> <span class="token number">21.80</span>s<span class="token punctuation">.</span> |
iOS | Android |
---|---|
![]() | ![]() |
Ví dụ trên làm gì?
Từ màn Login, bấm vào button 「Create an account」
login/toRegistrationScreenButton
để vào màn RegistrationTại màn Registration
1.1.Bấm vào nút 「Register」
registration/registerButton
, do máy Android của mình màn hơi ngắn, nên trước khi click phải scroll tới element đó trước.1.2. Do chưa fill gì nên expect error message hiển thị tương ứng
1.3. Fill 「user..01」vào input username, do field này chỉ nhận alphabet và số, nên giá trị đó ko hợp lệ, expect hiển thị error message
1.4. Fill username 1 giá trị hợp lệ 「user」
1.5. Fill password 「password」
1.6. Fill password confirmation「password123」, do ko match password đã nhập nên sẽ hiển thị error
1.7. Fill password confirmation hợp lệ 「password」
1.8. Fill full name
1.9. Bấm vào nút 「Register」
User được chuyển qua màn RegistrationCompleted, nên ta expect button ở màn này được hiển thị.
Phân tích một vài note về cú pháp
Cú pháp của WebdriverIO nhìn khá trong sáng và dễ hiểu, nên mình sẽ không giải thích nhiều, tuy nhiên có một vài chú ý dưới đây cho các bạn.
Khi tìm element, mình đã dùng
~
, đây là shorthand để tìm element theoaccessibility id
của WebdriverIO, tuỳ theo OS mà ta sẽ phải gán prop khác nhau để có thể dùng được strategy tìm theoaccessibility id
này link, chỉ vì cái này mà mình mất vài tiếng, ko hiểu tại sao ko tìm thấy element =))- Ở bên iOS ta phải dùng
testID
, và tránh gánaccessibilityLabel
vàaccessibilityHint
cho component. - Ở bên Android, hãy dùng
accessibilityLabel
- Các bạn có thể tham khảo các strategy khác ở docs của webdriverio link
- Ở bên iOS ta phải dùng
Tại sao lại dùng
accessibility id
mà không dùng content của nó?- Đúng ra là khi bấm button hay thao tác trên màn hình, chúng ta nên dùng text của nó, để khi text (requirement) thay đổi thì test cũng oẳng. Nhưng mà cơ bản là do
hơi bậnlười =)) và trường hợp accessibility id đúng nhưng text sai thì phải chấp nhận thôi à =))
- Đúng ra là khi bấm button hay thao tác trên màn hình, chúng ta nên dùng text của nó, để khi text (requirement) thay đổi thì test cũng oẳng. Nhưng mà cơ bản là do
Do API của WebdriverIO toàn trả về Promise, nên hầu như câu lệnh nào cũng cần phải dùng
async/await
, các bạn có thể sử dụng kèm package@wdio/sync
để API của WebdriverIO trở nên đồng bộ.Các bạn có để ý đoạn code ở
beforeEach
vàafterEach
không? Mỗi test, chúng ta đang tạo ra một session mới, và xoá session đó sau mỗi test. Nếu dùng CLI của wdio thì sẽ thuận tiện hơn.Options
xcodeOrgId
vàxcodeSigningId
bên trongcapabilities
củaRemoteOptions
iOSĐể chạy test trên thiết bị iOS, Appium sẽ cài một phần mềm tên là
WebDriverAgent
lên thiết bị, và để cài đặt được, ta phải cung cấp developer team và signing certificate.Các bạn có thể xem thêm tại đây link
Đối với thiết bị thật thì ta cần thêm UUID, có thể xem cách tìm UUID tại đây
capabilities
của AndroidĐể lấy
deviceName
, hãy chạyadb devices
com.learnrne2etest
và.MainActivity
có thể lấy từandroid/app/src/main/AndroidManifest.xml
1234567<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>manifest</span> <span class="token attr-name">package</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>com.learnrne2etest<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token comment"><!-- ... --></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>activity</span> <span class="token attr-name"><span class="token namespace">android:</span>name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>.MainActivity<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token comment"><!-- ... --></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>activity</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>manifest</span><span class="token punctuation">></span></span>Hoặc tham khảo bài sau link
Cưỡi ngựa xem hoa như vậy đủ rồi, chúng ta hãy cùng tìm hiểu thêm về cách hoạt động của Appium
3. Kiến trúc, flow của Appium
Appium dựa trên kiến trúc client-server, bản thân Appium là 1 server, có thể dễ dàng nhận thấy điều này ở đoạn config.
1 2 3 4 5 6 7 | <span class="token keyword">const</span> iOS<span class="token operator">:</span> RemoteOptions <span class="token operator">=</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">"/wd/hub"</span><span class="token punctuation">,</span> host<span class="token operator">:</span> <span class="token string">"localhost"</span><span class="token punctuation">,</span> port<span class="token operator">:</span> <span class="token number">4723</span><span class="token punctuation">,</span> <span class="token comment">// ...</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> |
3.1. Test library + WebDriver client
Khi viết test, ta sẽ dành phần lớn thời gian để làm việc với chúng, như ví dụ ở phần 2, ta đã sử dụng Jest (với các câu lệnh it
, describe
, afterEach
, afterAll
) là công cụ để chạy test, và WebdriverIO (với $
, click()
, setValue(value)
) để giao tiếp với Appium server.
Ngoài bộ đôi này, chúng ta có thể sử dụng bất cứ ngôn ngữ nào mà ta thích (miễn là Appium nó support :v), từ Ruby, Python cho đến PHP, C#, có thể xem list tại đây
Nhưng mà mình recommend combo sau:
Ngôn ngữ: JS/TS thân thiện với React Native dev
WebDriver client: WebdriverIO vì trong đám client, thằng này nhiều star trên github nhất =))
- Test library: jasmine
- Vì nó có cú pháp giống Jest, mà anh em code React Native thì quá quen thuộc với Jest rồi
- Là 1 trong 3 test framework được WebdriverIO support sẵn để go pro =)) (ngoài ra còn có mocha và cucumber), dùng Jest nếu lỗi thì phải tự mày mò thôi :v
Như đã nói ở trên, Appium áp dụng kiến trúc client-server. Khi ta viết những câu lệnh sau
1 2 3 | <span class="token comment">// ./__tests__/sign-up.spec.ts</span> <span class="token keyword">await</span> passwordConfirmationInput<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token string">"password123"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
nếu để ý log ta có thể thấy những dòng log dưới đây, webdriverio chỉ đơn thuần gửi 1 HTTP request tới server, server làm gì thì chúng ta sẽ tìm hiểu sau :v
- iOS
1 2 3 4 5 6 7 8 9 10 11 | <span class="token punctuation">[</span><span class="token constant">HTTP</span><span class="token punctuation">]</span> <span class="token operator">--</span><span class="token operator">></span> <span class="token constant">POST</span> <span class="token operator">/</span>wd<span class="token operator">/</span>hub<span class="token operator">/</span>session<span class="token operator">/</span>b891507e<span class="token operator">-</span><span class="token number">4</span>d84<span class="token operator">-</span><span class="token number">4e62</span><span class="token operator">-</span>a5b2<span class="token operator">-</span>e9110c529c9e<span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value <span class="token punctuation">[</span><span class="token constant">HTTP</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><span class="token string">"text"</span><span class="token operator">:</span><span class="token string">"password123"</span><span class="token punctuation">}</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">W3C</span> <span class="token punctuation">(</span>b891507e<span class="token punctuation">)</span><span class="token punctuation">]</span> Calling AppiumDriver<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">with</span> args<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"password123"</span><span class="token punctuation">,</span><span class="token string">"6F000000-0000-0000-E00E-000000000000"</span><span class="token punctuation">,</span><span class="token string">"b891507e-4d84-4e62-a5b2-e9110c529c9e"</span><span class="token punctuation">]</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span>XCUITest<span class="token punctuation">]</span> Executing command <span class="token string">'setValue'</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">WD</span> Proxy<span class="token punctuation">]</span> Matched <span class="token string">'/element/6F000000-0000-0000-E00E-000000000000/value'</span> to command name <span class="token string">'setValue'</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span>Protocol Converter<span class="token punctuation">]</span> Added <span class="token string">'text'</span> property <span class="token string">"password123"</span> to <span class="token string">'setValue'</span> request body <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">WD</span> Proxy<span class="token punctuation">]</span> Proxying <span class="token punctuation">[</span><span class="token constant">POST</span> <span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value<span class="token punctuation">]</span> to <span class="token punctuation">[</span><span class="token constant">POST</span> http<span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">127.0</span><span class="token number">.0</span><span class="token number">.1</span><span class="token operator">:</span><span class="token number">8100</span><span class="token operator">/</span>session<span class="token operator">/</span><span class="token number">01</span>BFCDD1<span class="token operator">-</span><span class="token number">27</span>D7<span class="token operator">-</span><span class="token number">4904</span><span class="token operator">-</span><span class="token constant">A95A</span><span class="token operator">-</span><span class="token constant">C75086994546</span><span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value<span class="token punctuation">]</span> <span class="token keyword">with</span> body<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token string">"value"</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token string">"p"</span><span class="token punctuation">,</span><span class="token string">"a"</span><span class="token punctuation">,</span><span class="token string">"s"</span><span class="token punctuation">,</span><span class="token string">"s"</span><span class="token punctuation">,</span><span class="token string">"w"</span><span class="token punctuation">,</span><span class="token string">"o"</span><span class="token punctuation">,</span><span class="token string">"r"</span><span class="token punctuation">,</span><span class="token string">"d"</span><span class="token punctuation">,</span><span class="token string">"1"</span><span class="token punctuation">,</span><span class="token string">"2"</span><span class="token punctuation">,</span><span class="token string">"3"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token string">"text"</span><span class="token operator">:</span><span class="token string">"password123"</span><span class="token punctuation">}</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">WD</span> Proxy<span class="token punctuation">]</span> Got response <span class="token keyword">with</span> status <span class="token number">200</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token string">"value"</span><span class="token operator">:</span><span class="token keyword">null</span><span class="token punctuation">,</span><span class="token string">"sessionId"</span><span class="token operator">:</span><span class="token string">"01BFCDD1-27D7-4904-A95A-C75086994546"</span><span class="token punctuation">}</span> <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">W3C</span> <span class="token punctuation">(</span>b891507e<span class="token punctuation">)</span><span class="token punctuation">]</span> Responding to client <span class="token keyword">with</span> driver<span class="token punctuation">.</span><span class="token function">setValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span> result<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">[</span><span class="token constant">HTTP</span><span class="token punctuation">]</span> <span class="token operator"><</span><span class="token operator">--</span> <span class="token constant">POST</span> <span class="token operator">/</span>wd<span class="token operator">/</span>hub<span class="token operator">/</span>session<span class="token operator">/</span>b891507e<span class="token operator">-</span><span class="token number">4</span>d84<span class="token operator">-</span><span class="token number">4e62</span><span class="token operator">-</span>a5b2<span class="token operator">-</span>e9110c529c9e<span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value <span class="token number">200</span> <span class="token number">821</span> ms <span class="token operator">-</span> <span class="token number">14</span> |
- Android
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 | console<span class="token punctuation">.</span>info <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.288</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token constant">COMMAND</span> <span class="token function">findElement</span><span class="token punctuation">(</span><span class="token string">"accessibility id"</span><span class="token punctuation">,</span> <span class="token string">"login/toRegistrationScreenButton"</span><span class="token punctuation">)</span> at node_modules<span class="token operator">/</span>@wdio<span class="token operator">/</span>logger<span class="token operator">/</span>build<span class="token operator">/</span>node<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">76</span><span class="token operator">:</span><span class="token number">9</span> console<span class="token punctuation">.</span>info <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.289</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token constant">POST</span><span class="token punctuation">]</span> http<span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>localhost<span class="token operator">:</span><span class="token number">4723</span><span class="token operator">/</span>wd<span class="token operator">/</span>hub<span class="token operator">/</span>session<span class="token operator">/</span><span class="token number">7</span>fc4b801<span class="token operator">-</span>deae<span class="token operator">-</span><span class="token number">49</span>dc<span class="token operator">-</span><span class="token number">947</span>d<span class="token operator">-</span><span class="token number">292</span>d67ba5467<span class="token operator">/</span>element at node_modules<span class="token operator">/</span>@wdio<span class="token operator">/</span>logger<span class="token operator">/</span>build<span class="token operator">/</span>node<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">76</span><span class="token operator">:</span><span class="token number">9</span> console<span class="token punctuation">.</span>info <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.289</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token constant">DATA</span> <span class="token punctuation">{</span> using<span class="token operator">:</span> <span class="token string">'accessibility id'</span><span class="token punctuation">,</span> value<span class="token operator">:</span> <span class="token string">'login/toRegistrationScreenButton'</span> <span class="token punctuation">}</span> at node_modules<span class="token operator">/</span>@wdio<span class="token operator">/</span>logger<span class="token operator">/</span>build<span class="token operator">/</span>node<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">76</span><span class="token operator">:</span><span class="token number">9</span> console<span class="token punctuation">.</span>info <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.414</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token constant">RESULT</span> <span class="token punctuation">{</span> <span class="token string">'element-6066-11e4-a52e-4f735466cecf'</span><span class="token operator">:</span> <span class="token string">'bb82f194-16d1-4ba0-a520-33f47e117211'</span><span class="token punctuation">,</span> <span class="token constant">ELEMENT</span><span class="token operator">:</span> <span class="token string">'bb82f194-16d1-4ba0-a520-33f47e117211'</span> <span class="token punctuation">}</span> at node_modules<span class="token operator">/</span>@wdio<span class="token operator">/</span>logger<span class="token operator">/</span>build<span class="token operator">/</span>node<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">76</span><span class="token operator">:</span><span class="token number">9</span> |
3.2. Appium server, Automation tool, Devices
Đây là phần xương sống trong kiến trúc của Appium, nó đảm nhận việc handle request từ client, giao tiếp với native automation tool để thực thi những command mà ta cần.
Appium server expose ra API theo chuẩn JSON Wire Protocol (WebDriver Protocol). Tuỳ vào target là iOS hay Android, nó sẽ có cách hoạt động riêng để phù hợp với nền tảng đó.
3.2.1. Appium meets iOS
Quay trở lại đoạn log ở phần 3.1, nó đã ít nhiều gợi ý cho ta cách hoạt động của Appium
1 2 3 | <span class="token punctuation">[</span><span class="token constant">HTTP</span><span class="token punctuation">]</span> <span class="token operator">--</span><span class="token operator">></span> <span class="token constant">POST</span> <span class="token operator">/</span>wd<span class="token operator">/</span>hub<span class="token operator">/</span>session<span class="token operator">/</span>b891507e<span class="token operator">-</span><span class="token number">4</span>d84<span class="token operator">-</span><span class="token number">4e62</span><span class="token operator">-</span>a5b2<span class="token operator">-</span>e9110c529c9e<span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value <span class="token punctuation">[</span>debug<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">WD</span> Proxy<span class="token punctuation">]</span> Proxying <span class="token punctuation">[</span><span class="token constant">POST</span> <span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value<span class="token punctuation">]</span> to <span class="token punctuation">[</span><span class="token constant">POST</span> http<span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">127.0</span><span class="token number">.0</span><span class="token number">.1</span><span class="token operator">:</span><span class="token number">8100</span><span class="token operator">/</span>session<span class="token operator">/</span><span class="token number">01</span>BFCDD1<span class="token operator">-</span><span class="token number">27</span>D7<span class="token operator">-</span><span class="token number">4904</span><span class="token operator">-</span><span class="token constant">A95A</span><span class="token operator">-</span><span class="token constant">C75086994546</span><span class="token operator">/</span>element<span class="token operator">/</span><span class="token number">6</span>F000000<span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token number">0000</span><span class="token operator">-</span><span class="token constant">E00E</span><span class="token operator">-</span><span class="token number">000000000000</span><span class="token operator">/</span>value<span class="token punctuation">]</span> |
Có thể dễ thấy Appium server đơn giản chỉ proxy request của ta tới 1 server khác chạy ở port 8100 http://127.0.0.1:8100
.
Oát dờ phước? Thằng nào đang chạy ở port 8100 vậy?
Đó là WebDriverAgent server, ban đầu được phát triển bởi ông lớn Facebook, appium đã fork về thêm mắm thêm muối gì đó vào.
WDA giúp giao tiếp với XCUITest để có thể điều khiển device/simulator iOS từ xa. Bản thân nó cũng đã support sẵn Webdriver Protocol, vậy nên chắc hẳn dev Appium đã nghĩ như sau
Tội gì mà phải code lại, có sẵn hàng ngon rồi thì dùng luôn thôi =))
WDA làm gì với device thì mình xin skip, vì cũng ko biết =)) Các bạn có thể tự mình tìm hiểu sâu hơn nếu có hứng thú với nó.
3.2.2. Appium meets Android
Với Android, ta có ít gợi ý hơn
1 2 3 4 5 6 | <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.289</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token constant">POST</span><span class="token punctuation">]</span> http<span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>localhost<span class="token operator">:</span><span class="token number">4723</span><span class="token operator">/</span>wd<span class="token operator">/</span>hub<span class="token operator">/</span>session<span class="token operator">/</span><span class="token number">7</span>fc4b801<span class="token operator">-</span>deae<span class="token operator">-</span><span class="token number">49</span>dc<span class="token operator">-</span><span class="token number">947</span>d<span class="token operator">-</span><span class="token number">292</span>d67ba5467<span class="token operator">/</span>element <span class="token number">2020</span><span class="token operator">-</span><span class="token number">12</span><span class="token operator">-</span><span class="token number">31</span>T02<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">23.414</span>Z <span class="token constant">INFO</span> webdriver<span class="token operator">:</span> <span class="token constant">RESULT</span> <span class="token punctuation">{</span> <span class="token string">'element-6066-11e4-a52e-4f735466cecf'</span><span class="token operator">:</span> <span class="token string">'bb82f194-16d1-4ba0-a520-33f47e117211'</span><span class="token punctuation">,</span> <span class="token constant">ELEMENT</span><span class="token operator">:</span> <span class="token string">'bb82f194-16d1-4ba0-a520-33f47e117211'</span> <span class="token punctuation">}</span> |
Không thấy proxy gì đó phải không nào? Đúng vậy, vì nó có proxy đâu :v
Ở version mới nhất, Appium sử dụng appium-android-driver để giao tiếp với UIAutomator2. Vậy giao tiếp thế nào?
Mỗi khi bắt đầu chạy test, Appium sẽ cài app Appium Settings trên device của ta, thằng này sẽ mở port 4724 trên device của ta và expose ra một vài API hệ thống. Appium server abc với app đã cài trên (qua HTTP), và nó sẽ xyz với UIAutomator2 để thực hiện các command.
4. Kết luận
Là một dev, chúng ta không được chủ quan, lệ thuộc vào bên tester, mà hãy tự mình kiểm thử trước khi chuyển qua cho họ. Test chạy cơm là một công việc nhàm chán, vậy thì tại sao chúng ta không làm cho nó thú vị hơn bằng cách automate nó. Tuy sẽ có lúc cần một số thủ thuật, hay là phải dùng nhiều command khá lằng nhằng ( như cái datepicker chẳng hạn =)) ), nhưng một khi chạy được nhất định nó sẽ đem lại cho các bạn một cảm giác phê như con tê tê =))
Code e2e của bài viết chỉ là những đoạn code tạm bợ, các bạn muốn go pro có thể tham khảo appium-boilerplate của webdriverio về cách chia module, cũng như setup project. Nếu có thời gian mình sẽ viết thêm về việc refactor đống code tạm bợ trên để go pro =)) Giờ thì xin tạm biệt các bạn.