Code standard trong Android

  1. Project guidelines.

    1.1. Cấu trúc project

Project mới có cấu trúc dựa theo Android Gradle project được định nghĩa ở  Android Gradle plugin user guide. Project ribot Boilerplate là 1 ví dụ khá tốt, các bạn có thể clone về và xem cách tổ chức source của project này.

1.2 Đặt tên file

        1.2.1 Tên class.

Ngoài các chuẩn khi đặt tên class trong ngôn ngữ java như:

  • Viết hoa chữ cái đầu.
  •  Viết  hoa các chữ bắt đầu 1 từ
  •  Tên phải có nghĩa với các method function của class.

ví dụ như: NetWorkUtils.java thì với các class extends từ các Android component, tên nên kết thúc là tên của component đó, ví dụ như:

LogInActivity, SignInFragment, ImageUploaderService, ChangePasswordDialog

1.2.2. Tên cho resources.

Tên file resources nên viết dưới dạng: lowercase_underscore.

1.2.2.1.Tên cho các Drawable files:

Tên cho các drawable file

8

file ảnh có đuôi .9.png là ảnh 9-patch khi dùng trong trên các màn hình có kích thước và độ phân giải khác nhau, ảnh sẽ tự resize, không bị vỡ giao diện, Search google để tìm hiểu thêm nhé 😀

Đặt tên cho các icon resources.

9.png

Tên  các files selector states:

10.png

1.2.2.2 Layout files.

Tên file layout nên match với tên Android components, tên component sẽ là tiền tố file layout ví dụ: Với activity SignInActivity thì file layout nên tên là activity_sign_in.xml.

11.png

Nếu tạo layout cho các item của Adapter listView hay RecycleVIew,ta nên đặt tiền tố cho file layout là _item.

Lưu ý có 1 số trường hợp các cách đặt tên trên không hoàn toàn được áp dụng. Ví dụ như khi tạo 1 layout file mà nó là 1 phần của file layout khác thì nên đặt tên file đó với tiền tố là partial_

1.2.2.3 Menu files.

Tương tự như các layout files, các menu files nên đặt tên match với component. Ví dụ,

Nếu chúng ta định nghĩa 1 menu file được sử dụng trong UserActivity, thì tên của nó nên là

activity_user.xml

Chúng ta không nên thêm từ menu vào tên file vì những file này đã được đặt trong thư mục menu trong cấu trúc ứng dụng rồi.

1.2.2.3 Value files

Resource files trong thư mục values nên ở dạng số nhiều tiếng anh: ex: strings.xml, styles.xml, colors.xml, dimens.xml, attrs.xml.

2.Code guidlines.

2.1  Code standard trong Java

2.1.1 không nên bỏ qua exceptions

Chúng ta không nên làm như sau:


void setServerPort(String value) {
 try {
 serverPort = Integer.parseInt(value);
 } catch (NumberFormatException e) { }
}

Chúng ta có thể nghĩ rằng đoạn code trên sẽ không bao giờ gây lỗi, hoặc không quan trọng để xứ lý nó, bỏ qua các exceptions loại khác như đoạn kết trên có thể tiềm ẩn khả năng gây lỗi và khó hiểu cho ng khác . Chúng ta nên xử lý tất cả các ngoại lệ theo các quy tác chuẩn tùy theo trường hợp của b.

Chúng ta có viết lại các đoạn code trên như sau:

    • ém ra 1 ngoại lệ tới nơi gọi method này:
      void setServerPort(String value) throws NumberFormatException {
       serverPort = Integer.parseInt(value);
      }
      

     

  • Ném ra 1 ngoại lệ tự định nghĩa

void setServerPort(String value) throws ConfigurationException {
 try {
     serverPort = Integer.parseInt(value);
 } catch (NumberFormatException e) {
         throw new ConfigurationException("Port " + value + " is not valid.");
 }
}

  • 1 cách xử lý khá tốt, ngay trong khối catch

/** Set port. If value is not a valid number, 80 is substituted. */

void setServerPort(String value) {
 try {
 serverPort = Integer.parseInt(value);
 } catch (NumberFormatException e) {
 serverPort = 80; // default port for server
 }
}

 

 

  • Bắt ngoại lệ và ném ra 1 RuntimeExeption. Khá nguy hiểm, nó chỉ nên dùng khi bạn chắc chắn rằng lỗi này  xẩy ra sẽ gây crash ứng dụng của bạn.

/** Set port. If value is not a valid number, die. */

void setServerPort(String value) {
 try {
 serverPort = Integer.parseInt(value);
 } catch (NumberFormatException e) {
 throw new RuntimeException("port " + value " is invalid, ", e);
 }
}

Lưu ý: với cách xử lý này compile với java 1.3 trở lên.

 

  • Nếu bạn chắc chắn rằng không xử lý exceptions này không ảnh hưởng tới ứng dụng, thì bạn cũng nên comment vào khối catch, để dễ dàng maintain sau này:
    /** If value is not a valid number, original port number is used. */
    void setServerPort(String value) {
     try {
     serverPort = Integer.parseInt(value);
     } catch (NumberFormatException e) {
     // Method is documented to just ignore invalid user input.
     // serverPort will just be unchanged.
     }
    }
    
    

    2.1.2 Không bắt lỗi 1 cách chung chung

Chúng ta không nên làm thế này:

try {
 someComplicatedIOFunction(); // may throw IOException
 someComplicatedParsingFunction(); // may throw ParsingException
 someComplicatedSecurityFunction(); // may throw SecurityException
 // phew, made it all the way
} catch (Exception e) { // I'll just catch all exceptions
 handleError(); // with one generic handler!
}

Chúng ta rất hay dính phốt này vì lười, nhưng nếu xẩy ra bug rất khó để chúng ta tìm

ra nguyên nhân để fix nhanh chóng.
Hãy bắt lỗi càng chi tiết càng tốt 🙂

2.1.3 Không sử dụng finalizers

Finalize là phương thức sẽ được gọi ngay trước khi hủy 1 đối tượng bởi Garbage Collector. Nó được sử dụng để chắc chắn rằng 1 đối tượng đã hoàn toàn kết thúc.

Ví dụ: sử dụng finalize() để đảm bảo rằng 1 file được mở mà sở hửu bởi 1 đối tượng nào đó đã được đóng.

Trong Android, chúng ta không nên sử dụng finalizers. Không có gì đảm bảo khi 1 finalizer sẽ được gọi, hay thậm chí nó sẽ được gọi mọi lúc. Trong hầu hết các trường hợp, bạn có thể làm những gì bạn cần từ finalizers với 1 xử lý ngoại lệ tốt. Nếu bạn vẫn cần nó, hãy đinh nghĩa phương thức close() và xác định chính xác khi nào phương thức này được gọi.

2.1.4 Vấn đề Import

  • không nên: import foo.*;
  • nên: import foo.Bar;

Chỉ import những class cần thiết, để hạn chế kích thước file class.

2.2 Styles code Java

2.2.1 Định nghĩa các biến và đặt tên

Các biến (trường) nên đặt ở đầu trong file, và nên được đặt tên theo 1 sốquy tắc sau đây:

  • các trường private, non-static nên được đăt tên bắt đầu bởi tiền tốm.
  • các trường private, static nên được đặt tên bắt đầu bởi tiền tố s.
  • các trường khác nên bắt đầu bởi chữ cái thường, và tên nên có ý nghĩa sử dụng
  • các trường static constants nên đặt tên gồm các chữ cái hoa và dấu gạch dưới

Ví dụ:


public class MyClass{

        private static final int SOME_CONSTANT = 42;

        public int numCout;

       private static MyClass sSingleton;

       private int mPrivate;

       protected int mProtected;
}

2.2.3 Một số quy tắc khác:
12.png

2.2.4 Sử dụng dấu cách cho thụt đầu dòng

Sử dụng “4 dấu cách liên tiếp” để thụt đầu dòng trong 1 khối code:

13.png

Sử dụng “8 dấu cách liên tiếp” để thụt đầu dòng khi 1 dòng lệnh quá dài.

14.png

2.2.5 Quy tắc code chuẩn trong các câu lệnh điều kiện if:

15.png

Nếu điều kiện và khối lệnh ở trên 1 dòng, nên không cần phải bọc trong cặp ký tự {}

Và nên:

16.png

Thay vì:
17.png

2.2.6 Annotations trong Java(Android)

2.2.6.1 Thực hành annotations

Theo như Android code style guidle, các annotations đã được định nghĩa sẵn gồm

  @override: được sử dụng khi 1 phương thức overrides 1 phương thức từ class cha.

@SuppressWarnings: nên được sử dụng trong những  tình huống không thể loại bỏ  các cảnh báo. Nếu các cảnh báo là không thể loại bỏ, annotations này phải được sử dụng để đảm bảo rằng tất cả các các cảnh báo phản ánh đúng vấn đề thực sự trong code.

2.2.6.2 quy tắc của annotations

Class, method và Constructors

Khi áp dụng annotations trong class, method hay constructor, chúng được liệt kê vào đoạn chú thích mô tả và mỗi annotations trên 1 dòng:

18.png

Biến, trường

Annotations áp dụng vào các biến (trường) nên được đặt trên 1 dòng, trừ khi nó vượt quá giới hạn 1 kí tự trên 1 dòng code.

19.png

2.2.7 Phạm vi của biến

Phạm vi của các biến cục bộ nên giữ nhỏ nhất có thể. Điều này làm code của bạn đễ đọc và dễ bảo trì hơn, giảm thiểu khả năng gây lỗi. Biến nên được khai báo trong đoạn mã  code mà biến này được sử dụng.

Các biến cục bộ nên được khai báo lúc nó được sử dụng. Hầu hết mỗi biến được khai báo đều cần khởi tạo. Do đó, nếu chưa đủ các thông tin để khởi tạo biến, tốt nhất là chưa nên khái báo biến cho tới khi thực sự cần.

2.2.8 Vấn đề về import trong Android:

Nếu sử dụng IDE Android studio, chúng ta không cần lo lắng điều này vì AS tự động import và tuân theo các quy tắc viết code đã nói ở trên.

Android studio cung cấp các Import gồm:

  • Android imports
  • Import từ lib bên thứ 3 (com, junit, net, org)
  • java và javax
  • Import project mẫu

để phù hợp với setting của As, các import nên:
– theo thứ tự bảng chứ cái trong mỗi nhóm, với chữ viết hoa trước chữ thường.

– nên có 1 dòng trống (cách dòng) trước mỗi nhóm chính (android, com, junit, net, org, java, javax)

2.2.9 hướng dẫn hiển thị Log

Sử dụng phương thức loggin được cung cấp bởi class Log để in ra các thông báo lỗi hoặc các  thông tin hữu ích cho việc phát hiện vấn đề khi coding.

20.png

Ta nên sử dụng tên class như là tag và ta định nghĩa 1 biến static final ở đầu file. ví dụ:

21.png

Verbosedebug logs phải loại bỏ khi ta sẵn sàng tạo bản build phát hành. Ta cũng nên loại bỏ cả các log khác nhưng cũng có thể cho phép nếu nó hữu ích trong việc xác định các vấn đề gây lỗi trong bản phát hành. Nếu bạn để các loại log này được hiển thị, bạn phải chắc chắn rằng chúng không rò rỉ các thông tin nhạy cảm của người dùng như email, địa chỉ….

Chỉ nên show log ở chế độ debug:

22.png

2.2.10 Tổ chức Class.

Không có 1 solution chính thức nào, nhưng tổ chức cách thành phần trong class thống nhất và ít thay đổi rất tốt cho khả năng maintain code sau này. Sau đây là cấu trúc được khuyến nghị.

  1. Các giá trị constants (hằng số).
  2. Các biến (trường)
  3. Các phương thức khởi dựng (constructors)
  4. Các phương thức override và callback (public hoặc private).
  5. Các phương thức public.
  6. Các phương thức private.
  7. Các class nội và Interfaces.

Ví dụ:


public class MainActivity extends Activity {
private static final int CONSTANT_VAL = 12;
private String mTitle;
private TextView mTvTitle;
public String status;

@Override
public void onCreate() {
...
}
public void setTitle(String title) {
mTitle = title;
}

private void setUpView() {
...
}

static class AnInnerClass {

}

}

Nếu class của chúng ta extends từ một Android component như Activity hay Fragment, 

Chúng ta nên override các phương thức phù hợp với vòng đời của component đó. Ví dụ, chúng ta có 1 Activity, ta implements onCreate(), onResume(), onPause(), onDestroy(), đoạn code chi tiết như sau:


public class MainActivity extends Activity {

//Order matches Activity lifecycle
@Override
public void onCreate() {}

@Override
public void onResume() {}

@Override
public void onPause() {}

@Override
public void onDestroy() {}

}

2.2.11 Truyền tham số trong method

Trong lập trình Android, thường rất hay có các phương thức truyền vào biến Context. Nếu bạn có các phương thức kiểu này, biến Context nên là biến đầu tiên.

Ngược lại các interfaces callback nên là tham số cuối cùng.

24.png

2.2.12. Chuỗi không đổi, tên và giá trị.

Nhiều yếu tố cỉa Android SDK như SharePreferences, Bundle, hay Intent sử dụng 1 cặp key-value, các yếu tố này có trong hầu hết các loại ứng dụng, và chúng ta phải viết rất nhiều các chuỗi không đổi.

Khi sử dụng các yếu tố này, ta nên định nghĩa giá trị key như 1 static final và nên đặt các tiền tố như bảng sau:

25.png

Lưu ý rằng: tham số của Fragment – Fragment.getArguments() cũng là 1 bundle, nhưng do nó rất hay được sử dụng nên ta định nghĩa 1 tiền tố khác cho nó.

Ví dụ:


// Note the value of the field is the same as the name to avoid duplication issues
static final String PREF_EMAIL = "PREF_EMAIL";
static final String BUNDLE_AGE = "BUNDLE_AGE";
static final String ARGUMENT_USER_ID = "ARGUMENT_USER_ID";

// Intent-related items use full package name as value
static final String EXTRA_SURNAME = "com.myapp.extras.EXTRA_SURNAME";
static final String ACTION_OPEN_USER = "com.myapp.action.ACTION_OPEN_USER";

2.2.13 Tham số trong Fragment và Activities.

Khi passed Data tới Activity hay Fragment thông qua Intent hay Bundle, key và values nên tuân theo các quy tắc đã định nghĩa trên.

Khi 1 Activiy hay Fragment mong đợi các tham số, nó nên cung cấp 1 phương thức public static, để tạo các intent hay fragment thích hợp.

Ví dụ: với trường hợp là Activity, phương thức này nên gọi là getStartIntent():


public static Intent getStartIntent(Context context, User user) {
 Intent intent = new Intent(context, ThisActivity.class);
 intent.putParcelableExtra(EXTRA_USER, user);
 return intent;
}

Với Fragment tên nên là newInstance(), phương thức xử lý việc khởi tạo Fragment với các tham số chính xác.


public static UserFragment newInstance(User user) {
 UserFragment fragment = new UserFragment();
 Bundle args = new Bundle();
 args.putParcelable(ARGUMENT_USER, user);
 fragment.setArguments(args)
 return fragment;
}

Lưu ý 1: những phương thức này nên đặt ở trước onCreate().

Lưu ý 2: Nếu ta định nghĩa các method trên, key của  tham số nên để là private để hạn chế truy cập từ bên ngoài class.

2.2.14 Giới hạn độ dài dòng code.

1 dòng code không nên quá 100 kí tự. Nếu vượt quá, có 2 cách để giảm độ dài như sau:

– rút gọn tên biến hay phương thức (hay dùng)

–  Tách 1 dòng viết trên nhiều dòng

Có 1 số ngoại lệ có thể vượt quá 100 kí tự như:

  •   1 đường link nào đấy, không thể tách được
  • tên package hay import…

2.2.14.1

Không có 1 cách thức nào để xuống tách 1 dòng code quá dài. thường khá có nhiều cách. Tuy nhiên có 1 vài quy tắc chung được áp dụng ở hầu hết các trường hợp.

Tách dòng trước toán tử.


int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
 + theFinalOne;

Ngoại lệ của toán tử gán:
1 ngoại lê của quy tắc xuống dòng ở trên là khi dùng với toán tử gán “=”, Dòng code nên được tách sau toán tử này.


int longName =
 anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;

Trường hợp gọi chuỗi các phương thức.

Khi gọi 1 chuỗi các phương thức liên tiếp trên 1 dòng, chẳng hạn như khi sửa dụng Builders, mỗi lần gọi, 1 phương thức nên ở trên 1 dòng và được tách bởi dấu “.

26.png

Trường hợp tham số quá dài.

Khi gọi 1 phương thức có nhiều tham số hoặc tham số quá dài, ta nên tách dòng sau mỗi dấu “,“.

27.png

2.2.16 Quy tắc của chuỗi RxJava

Chuỗi Rx của toán tử yêu cầu tách dòng. Mỗi toán tử phải bắt đầu trên 1 hàng và hàng nên được tách dòng bởi dấu “.

28

2.3 Các quy tắc viết XML

2.3.1 Nên dùng dấu tự  đóng tag.

Khi sử dụng 1 phần tử XML mà không có nội dung gì, nên dùng dấu tự đóng tag.

Nên:

29.png

Không nên:

30.png

2.3.2 Tên các resources

Resouce Id và tên phải được viết gồm các chữ thường và dấu gạch dưới.

31.png

ví dụ: menu

32.png

2.3.2.2 Chuỗi (strings)

tên chuỗi nên được đặt với tiền tố xác định thành phần mà chuỗi này thuộc về.Ví dụ:

registration_email_hint hay registration_name_hint . Nếu các chuỗi không thuộc bất cứ thành phần nào thì nên theo 1 số quy tắc dưới đây:
33.png

2.3.2.3 Styles và Themes

Ngoại trừ cá thành phần của resouces, tên của style nên được đặt giống như tên class trong Java (ví dụ tên của 1 styles: UpperCamelCase.)

2.3.3 Thứ tự các thuộc tính trong xml

Như 1 quy tắc chung, chúng ta nên đặt các thuộc tính cùng nhau trong 1 file XML, các thuộc tính này nên được sắp xếp theo thứ tự sau:

  1. View Id
  2. Style
  3. layout with và layout height
  4. Các thuộc tính layout khác(sắp xếp theo abc)
  5. Các thuộc tính còn lại (sắp xếp theo abc)

2.4 Quy tắc viết Test trong Android

2.4.1 Unit tests

Class test nên khớp với tên của class cần test và kết thúc bởi hâu tố Test. Ví dụ, để tạo class để test cho class DatabaseHelper, ta nên đặt tên cho class này là DatabaseHelperTest.

Phương thức test nên đặt annotations là @Test và nên bắt đầu với tên của phương thức cần test, theo sau bởi 1 điều kiện tiên quyết hay 1 kết quả mong đợi.

  • Mẫu: @Test void methodNamePreconditionExpectedBehavior();
  • Ví dụ: @Test void signInWithEmptyEmailFails();

Điều kiện tiên quết hay kết quả mong đợi có thể không cần xuất hiện trong đặt tên nếu code trong phương thức test rõ ràng, dễ hiểu.

Trong một số trường hợp, class có thể chứa rất nhiều các phương thức, trong cùng 1 thời gian đòi hỏi nhiều test case cho mỗi phương thức. Trường hợp này, ta nên tách class test này thành nhiều class test liên quan. Ví dụ, class DataManager có nhiều phương thức, chúng ta có thể phân tách thành các class để test như DataManagerSignInTest, DataManagerLoadUserTest….

2.4.2 Espresso Test

Mỗi class Espresso test thường sửa dụng cho một Activity nào đấy, do đó tên của nó nên khớp với Activty, kết thúc bởi hậu tố Test, ví dụ như: SignInActivityTest

Khi sửa dụng Espresso API, thông thường ta sẽ gọi một chuỗi các phương thức, các phương thức trên 1 dòng và được tách dòng bởi dấu “.“.


onView(withId(R.id.view))
.perform(scrollTo())
.check(matches(isDisplayed()))

Kiểm thử UI Android với Espresso.

  1.  Espresso test framework

Espresso là một framework kiểm thử giao diện cho Android được xây dựng để viết các test case giao diện một cách dễ dàng và đáng tin cậy.

Được giới thiệu bời Google vào tháng 10, 2013. Từ phiên bản 2.0, Espresso đã được tích hợp vào Android Support Repository.

Espresso tự động đồng bộ các tương tác với giao diện người dùng trên application của chúng ta. Framwork này cũng đảm bảo rằng Activity tương ứng được start trước khi chạy các test case.Các test case sẽ đợi cho tới khi tất cả các activity background được hoàn tất.

Được thiết kếđể kiểm thử 1 ứng dụng độc lập nhưng nó cũng có thể dùng để kiểm thử across application. Nếu sử dụng để kiểm thử ngoài ứng dụng của bạn. Bạn chỉ có thể thực hiện kiểm thử hộp đen (black box testing), vì bạn không thể truy cập các class ngoài ứng dụng.

Espresso về cơ bản có 3 thành phần:

  • ViewMatcher – cho phép tìm đến view (Control) trong hệ thống cấp bậc view(Giao diện)
  • ViewActions –  cho phép thực hiện tương tác lên view
  • ViewAssertions – cho phép kiểm tra, xác nhận trạng thái của view

Cấu trúc của Esspresso test như sau:

Untitled.png

  1. – Tìm thành phần giao diện (view) tương ứng.
  2. – thực hiện tương tác trên giao diện (view)
  3. – xác nhận trạng thái giao diện (view).

Đoạn code sau sẽ chứng minh cách sử dụng Espresso framework.

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

// image more code here...

// test statement
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
 .perform(click()) // click() is a ViewAction
 .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion

// new test
onView(withId(R.id.greet_button))
.perform(click())
.check(matches(not(isEnabled()));
 

Nếu  espresso không tìm thấy view tương ứng trong ViewMatcher, nó sẽ chứa tất cả hệ thống cấp bậc view  vào trong message lỗi. Rất dễ đàng chúng ta để phát hiện ra vấn đề gây lỗi.

2. Sử dụng Espresso.

2.1 cài đặt.

Mở Android SDK manager và cài đặt Android support repository.

espresso

2.2 Cấu hình Espresso trong Gradle build file

Để sử dụng Espresso, thêm dependency sau vào Gradle build file trên project của bạn

dependencies {
 compile fileTree(dir: 'libs', include: ['*.jar'])

 testCompile 'junit:junit:4.12'

 // Android runner and rules support
 androidTestCompile 'com.android.support.test:runner:0.5'
 androidTestCompile 'com.android.support.test:rules:0.5'

 // Espresso support
 androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
 exclude group: 'com.android.support', module: 'support-annotations'
 })

 // add this for intent mocking support
 androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'

 // add this for webview testing support
 androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2.2'

}

Đảm bảo rằng android.support.test.runner.AndroidJUnitRunner đặt trong testInstrumentationRunner trong gradle build file. Và packagingOptions, bạn có thể loại trừ LICENSE.txt, phụ thuộc vào libraries bạn sử dụng. Như ví dụ dưới đây.


apply plugin: 'com.android.application'

android {
compileSdkVersion 23
buildToolsVersion "23.0.3"

defaultConfig {
applicationId "com.bk.haodv.espressofirst"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
// android.support.test.runner.AndroidJUnitRunner
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
packagingOptions {
exclude 'LICENSE.txt'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
//as before...
}

2.3 Cài đặt device

Chúng ta nên tắt hiệu ứng animation trên thiết bị để chạy Espresso test. Animation có thể gây khó hiểu Espresso khi chạy không tải.

image.png

3. Ứng dụng Espresso đầu tiên.

3.1 tạo project

Tạo 1 Android project tên là Espresso First với package name com.bk.haodv.espressofirst chọn mẫu Blank template.

Thay đổi activity_main.xml với các control

</pre>
<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.bk.haodv.esspressotwo.MainActivity"&gt;


    &lt;EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/edt_input"
        android:layout_alignParentTop="true"
        android:hint="please input text"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="52dp" /&gt;

    &lt;Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start new activity"
        android:id="@+id/btn_start"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" /&gt;
&lt;/RelativeLayout&gt;
</pre>
<pre>

Tạo thêm 1 file giao diện activity_second.xml


&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:paddingBottom="@dimen/activity_vertical_margin"
 android:paddingLeft="@dimen/activity_horizontal_margin"
 android:paddingRight="@dimen/activity_horizontal_margin"
 android:paddingTop="@dimen/activity_vertical_margin"
 android:gravity="center"
 tools:context="com.bk.haodv.esspressotwo.SecondActivity"&gt;

 &lt;TextView
 android:id="@+id/tv_result"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Started"/&gt;

&lt;/RelativeLayout&gt;

Tạo 1 activity mới tên là SecondActivity với nội dung như sau:


public class SecondActivity extends AppCompatActivity {

     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_second);
         TextView viewById = (TextView) findViewById(R.id.tv_result);
         Bundle inputData = getIntent().getExtras();
         String input = inputData.getString(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;input&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;);
         viewById.setText(input);
    }
}

Trong MainActivity class:


package com.bk.haodv.espressofirst;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {
EditText editInput;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews(){
editInput = (EditText) findViewById(R.id.edt_input);
}

public void onClick(View view){
switch (view.getId()){
case R.id.btn_change_text:
editInput.setText(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Lalala&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;);
break;
case R.id.btn_swith_activity:
Intent intent = new Intent(this, SecondActivity.class);
Bundle bundle = new Bundle();
bundle.putString(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;input&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;, editInput.getText().toString());
intent.putExtras(bundle);
startActivity(intent);
break;
}
}
}

3.2 Cấu hình Espresso trong build.gradle


apply plugin: 'com.android.application'

android {
compileSdkVersion 23
buildToolsVersion &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;23.0.3&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;

defaultConfig {
applicationId &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;com.bk.haodv.espressofirst&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;1.0&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;
// android.support.test.runner.AndroidJUnitRunner
testInstrumentationRunner &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;android.support.test.runner.AndroidJUnitRunner&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;
}
packagingOptions {
exclude 'LICENSE.txt'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
// Android runner and rules support
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
}

3.3 Tạo Esoresso test case.

Tạo clas MainActivityTest trong package androidTest để kiểm thử MainActivity.


package com.bk.haodv.espressofirst;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;

import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

/**
* Created by T430 on 9/1/2016.
*/

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

@Rule
public ActivityTestRule&amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; mActivityRule =
new ActivityTestRule&amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;(MainActivity.class);

@Test
public void ensureTextChangesWork() {
// nhập vào chuỗi hello và nhấn vào button
onView(withId(R.id.edt_input))
.perform(typeText(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;HELLO&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;), closeSoftKeyboard());
onView(withId(R.id.btn_change_text)).perform(click());

// Kiểm tra xem khi click button có đúng chuỗi &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Lalala&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; hay không (kết quả mong đợi)
onView(withId(R.id.edt_input)).check(matches(withText(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Lalala&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)));
}
@Test
public void switch_newActivity() {
//&amp;amp;amp;amp;amp;amp;amp;amp;nbsp;Nhập vào dòng NewText rồi nhấn button &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;Change Text&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;
onView(withId(R.id.edt_input)).perform(typeText(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;NewText&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;),
closeSoftKeyboard());
onView(withId(R.id.btn_swith_activity)).perform(click());

// check xem ở SecondActivity có set đúng string &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;NewText&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; truyền từ MainActivity sang

// TextView &amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;tv_result&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; ở Activity khác, nhưng Espresso sẽ tự match được, không cần khai báo gì thêm
onView(withId(R.id.tv_result)).check(matches(withText(&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;NewText&amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;)));
}
}

3.4 Chạy các test case:

Chuột phải vào class MainActivityTest chọn Run:

test-pass

Như kết quả thông báo : đã pass cả 2 test case, nếu có test case nào đó không pass, sẽ hiển thị đường màu đỏ thay vì màu xanh và rất dễ dàng tìm ra test case sai để còn Fix :D.

4. Viết thêm các test case khác với Espresso.

4.1 . Vị trí của các class Espresso tests và các static imports cần thiết

Class Espresso tests phải được đặt trong app/src/androidTest folder.

Để dễ sử dụng các API của Espresso. Chúng ta nên thêm các static imports sau. Cho phép truy cập các method mà không cần tiền tố class.


import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

4.2. Sử dụng ViewMatcher

Để tìm 1 thành phần giao diện (view) sửa dụng phương thức onView() với 1 view matcher sẽ lấy chính xác view cần thiết. Nếu sử dụng AdapterView , ta sử dụng onData()  thay vì onView() . Phương thức onView() sẽ trả về 1 đối tượng loại ViewInteraction. phương thức onData() sẽ trả về 1 đối tượng loại DataInteraction.

Bảng dưới đây mô tả các matchers dành sẵn:

2

4.3. Tương tác trên View

ViewInteraction DataInteraction cho phép tương tác thông qua 1 đối tượng thuộc loại ViewAction với phương thức perform. ViewActions class cung cấp các method helper cho các actions phổ biến như:

  • ViewActions.click
  • ViewActions.typeText()
  • ViewActions.pressKey()
  • ViewActions.clearText()

Phương thức perform trả về 1 đối tượng thuộc ViewInteraction . Bạn có thể thực hiện thêm nhiều action để xác nhận các kết quả mong đợi. có thể thêm 1 số tham số. ví dụ, bạn có thể thực hiện nhiều actions cùng 1 lúc…

4.4 Xác thực các kết quả test.

Gọi phương thức ViewInteraction.check() để xác định trạng thái của view đã thực hiện tương tác. Phương thức này mong đợi 1 đối tượng đầu vào ViewAssertion. ViewAssertions class cung cấp các phương thức helper  để tạo những đối tượng này.

  • matchers – Hamcrest matcher
  • doesNotExist – xác định rằng view đã chọn không tồn tại

Bạn cũng có thể sử dụng Hamcrest matchers (mạnh hơn), như ví dụ dưới đây :

3.png

  1. Matches đến view, có text bắt đầu với chuỗi “ABC”.
  2. Matches đến view  có text kết thúc bởi chuỗi “YYZZ”
  3. Matches đến text của view xác định bởi R.id có chứa nội dung mô tả chứa chuỗi “YYZZ” ở bất kỳ vị trí nào.
  4. Matches đến view có text bằng với chuỗi string tham số, bỏ qua hoa thường
  5. Matches đến view có text bằng với chuỗi string tham số, bỏ qua khoảng trắng
  6. Match đến view xác đinh bởi R.id mà text không chứa chuỗi “YYZZ”.

4.5. Truy cập các API

Thông qua phương thức InstrumentationRegistry.getTargetContext(), bạn có thể truy cập đến context hiện tại trên ứng dụng của ban. VÍ dụ, nếu muốn dùng id mà không cần dùng R.id, bạn có thể sửa dụng phương thức helper sau để xác định nó.


package testing.android.vogella.com.asynctask;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

@RunWith(AndroidJUnit4.class)
public class EspressoTest {

@Rule
 public ActivityTestRule&amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;gt; mActivityRule =
 new ActivityTestRule&amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;gt;(MainActivity.class);

@Test
 public void buttonShouldUpdateText(){
 onView(withId(R.id.update)).perform(click());
 onView(withId(getResourceId("Click"))).check(matches(withText("Done")));
 }

private static int getResourceId(String s) {
 Context targetContext = InstrumentationRegistry.getTargetContext();
 String packageName = targetContext.getPackageName();
 return targetContext.getResources().getIdentifier(s, "id", packageName);
 }
}

4.6. Cấu hình để start intent cho Activity

Nếu tham số thứ 3 là false trong ActivityTestRule, bạn có thể cấu hình cho intent để start activity. Hãy theo dõi ví dụ sau:


package com.vogella.android.testing.espressosamples;

import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

@RunWith(AndroidJUnit4.class)
public class SecondActivityTest {

@Rule

// tham số thứ 3 set là false nghĩa là activity không được tự động start
 public ActivityTestRule&amp;amp;amp;amp;amp;lt;SecondActivity&amp;amp;amp;amp;amp;gt; rule =
 new ActivityTestRule(SecondActivity.class, true, false);

@Test
 public void demonstrateIntentPrep() {
 Intent intent = new Intent();
 intent.putExtra("EXTRA", "Test");
 rule.launchActivity(intent);
 onView(withId(R.id.display)).check(matches(withText("Test")));
 }
}

4.7. Ghi hình phiên kiểm thử Espresso UI.

Android studio cung cấp 1 options Run-> Record Espresso Test trên thanh menu cho phép ghi lại tương tác với ứng dụng của bạn và tạo test case Espresso từ nó.

Tính năng này xuất hiện từ bản Android studio 2.2 preview 3.

4

5.png

4.8. Cấu hình Activity để thực hiện Test

Bạn cũng có thể truy cập tới Activity bạn đang test và gọi các phương thức của nó. Ví dụ:


public class MainActivity extends Activity {

@Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 }
 public void configureMainActivity(String Uri) {
 // do something with this
 }
}

Phương thức configureMainActivity có thể được như sau:


import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)

public class ExampleInstrumentedTest {

@Rule
 public ActivityTestRule&amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;gt; mActivityRule =
 new ActivityTestRule&amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;gt;(MainActivity.class);

@Test
 public void useAppContext() throws Exception {
 MainActivity activity = mActivityRule.getActivity();
 activity.configureMainActivity("http://www.vogella.com");
 // do more
 }
}

Lưu ý: Chúng ta cũng có thể override các phương thức trong ActivityTestRule, ví dụ như các phương thứcbeforeActivityLaunchedafterActivityLaunched …

Bạn cũng có thể truy cập đến Activity hiện hành.


@Test
public void navigate() {

Activity instance = getActivityInstance();
 onView(withText("Next")).perform(click());
 Activity activity = getActivityInstance();
 boolean b = (activity instanceof SecondActivity);
 assertTrue(b);
 // do more
 }

public Activity getActivityInstance() { 
 final Activity[] activity = new Activity[1];
 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable( ) {
 public void run() {
 Activity currentActivity = null;
 Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
 if (resumedActivities.iterator().hasNext()){
 currentActivity = (Activity) resumedActivities.iterator().next();
 activity[0] = currentActivity;
 }
 }
 });

return activity[0];
 }

Phương thức getActivityInstance cho phép truy cập tới activity hiện tại đang active.

Lưu ý: ActivityLifecycleMonitorRegistry không phải là API vì vậy nó có thể thay đổi.

4.9 Chạy các Espresso tests

4.9.1 Sử dụng Android studio

Click vào class Espresso và chọn Run

6.png

4.9.2. Sử dụng gradle

Sử dụng connectedCheck task trong Gradle để chạy espresso test case.

7.png

4.10. Kiểm tra 1 Toast.

Ví dụ dưới đây, chúng ta sẽ click lên 1 list item và kiểm tra 1 Toast được hiển thị lên.


import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
public class MainActivityTestList {

@Rule
 public ActivityTestRule&amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;gt; rule = new ActivityTestRule&amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;gt;(MainActivity.class);

@Test
 public void ensureListViewIsPresent() throws Exception {
 onData(hasToString(containsString("Frodo"))).perform(click());
 onView(withText(startsWith("Clicked:"))).
 inRoot(withDecorView(
 not(is(rule.getActivity().
 getWindow().getDecorView())))).
 check(matches(isDisplayed()));
 }
}

5. Mocking intents với Espresso Intents

Espresso cũng cung cấp 1 option để mock intents. Cho phép kiểm tra nếu 1 activity đã start intent và phản ứng đúng nếu nhận được các kết quả trả về từ intent là chính xác

Espresso intents được cung cấp bởi com.android.support.test.espresso:espresso-intents.  Để sự dụng, tấu hình trong build.gradle.

Nếu muôn sử dụng Espresso intent trong test case của bạn. SỬ dụng IntentTestRule thay vì ActivityTestRule.


import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.toPackage;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

@RunWith(AndroidJUnit4.class)
public class TestIntent {

@Rule
 public IntentsTestRule&amp;amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;amp;gt; mActivityRule =
 new IntentsTestRule&amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;gt;(MainActivity.class);

@Test
 public void triggerIntentTest() {
 onView(withId(R.id.button)).perform(click());
 intended(toPackage("testing.android.vogella.com.simpleactivity"));
 }

}

Tham khảo thêm tại:
https://code.google.com/p/android-test-kit/wiki/EspressoIntentsSetupInstructions.

6. Ứng dụng thứ 2: VIết test case cho Intent với Espresso.

6.1. Tạo 1 project tên là Espresso Two, MainActivity có 1 layout gồm 1 EditText và 1 Button,

Tạo thêm 1 SecondActivity với layout có 1 TextView hiển thị kết quả

khi nhấn vào button ở MainActivty, SecondActivity sẽ start và truyền 1 string extras vào intent với key là “URL” , chuỗi truyền là “haobk4dev.wordpress.com” chẳng hạn 🙂

MainActivity:


package com.bk.haodv.esspressotwo;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

Button btnStart;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 btnStart = (Button)findViewById(R.id.btn_start);
 btnStart.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View view) {
 startNewActivity();
 }
 });
 }
 private void startNewActivity(){
 Intent intent = new Intent(getApplicationContext(), SecondActivity.class);
 intent.putExtra("URL", "haobk4dev.wordpress.com");
 startActivity(intent);
 }
}

6.2. Viết các test case.

Viết các test case cho project này:

  • Kiểm tra layout của MainActivity có chứ buttton không?
  • Đảm bảo rằng text trên button là “Start new activity”.
  • Đảm bảo rằng nếu phương thức getActiviy.startNewActivity() được gọi. Nó sẽ kích hoạt đúng intent, và intent phải chứa string extra (hasExtra(“URL”, “haobk4dev.wordpress.com”)).

 

6.3. Xác nhận các kết quả test

Test code của chúng ta cho các test case ở trên như sau:

package com.bk.haodv.esspressotwo;

/**
 * Created by T430 on 9/7/2016.
 */

import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static android.support.test.espresso.intent.matcher.IntentMatchers.toPackage;
import static org.hamcrest.core.IsNull.notNullValue;

@RunWith(AndroidJUnit4.class)
public class TestIntent {

 @Rule
 public IntentsTestRule&amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;gt; mActivityRule = new IntentsTestRule&amp;amp;amp;amp;lt;MainActivity&amp;amp;amp;amp;gt;(MainActivity.class);

 @Test
 public void triggerIntentTest() {
 // kiểm tra xem có btn_stat trong layout của MainActivty hay ko?
 onView(withId(R.id.btn_start)).check(matches(notNullValue()));
 // CHeck xem text của button trên có phải là Start new activity hay ko?
 onView(withId(R.id.btn_start)).check(matches(withText("Start new activity")));
 // check xem click vào button có start intent và extras có chính xác hay ko?
 // click vao button
 onView(withId(R.id.btn_start)).perform(click());
 // check xem start dung activity hay ko?
 intended(toPackage("com.bk.haodv.esspressotwo"));
 // check xem truyen dung string extras không?
 intended(hasExtra("URL", "http://www.vogella.com"));
 }
}

7. Ứng dụng thứ 3: Kiểm thử chức năng cho các Activity.

7.1 Mục đích:

Chúng ta sẽ start Activity và đánh giá các kết quả test:

7.2. Viết các test case chức năng cho activities.


package com.bk.haodv.esspressotwo;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

/**
 * Created by T430 on 9/7/2016.
 */
@RunWith(AndroidJUnit4.class)
public class TestSecondActivityIsStarted {

@Rule
 public ActivityTestRule&amp;amp;amp;lt;MainActivity&amp;amp;amp;gt; mActivityRule = new ActivityTestRule&amp;amp;amp;lt;MainActivity&amp;amp;amp;gt;(MainActivity.class);

@Test
 public void validateSecondActivity(){
 // click button trong MainActivity
 onView(withId(R.id.btn_start)).perform(click());
 // Check xem da start SecondActivity chinh xac
 onView(withId(R.id.tv_result)).check(matches(withText("Started")));

}
}

Để kiểm thử sự biến đổi của view,chúng ta tạo thêm  classs test cho SeconActivity class:


package com.bk.haodv.esspressotwo;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
/**
 * Created by T430 on 9/7/2016.
 */
@RunWith(AndroidJUnit4.class)
public class SecondActivityFunctionalTest {

@Rule
 public ActivityTestRule&amp;amp;amp;lt;MainActivity&amp;amp;amp;gt; mActivityRule = new ActivityTestRule&amp;amp;amp;lt;MainActivity&amp;amp;amp;gt;(MainActivity.class);

@Test
 public void validateSecondActivity(){
 // Click button tren MainActivity
 onView(withId(R.id.btn_start)).perform(click());
 // check textView tren SecondActivity
 onView(withId(R.id.tv_result)).check(matches(withText("Started")));
 pressBack();
 onView(withId(R.id.btn_start)).check(matches(withText("Start new activity")));
 }
}

8. Sử dụng Espresso để test asynchronous code

Kiểm thử đồng bộ mà không có framework hỗ trợ là khá khó khăn. Cách tiếp cận trước Espresso là đợi 1 khoảng thời gian xác định hoặc sử dụng 1 instance của CountDownLatch class trong class test case của chúng ta và dấu hiệu từ tiến trình đồng bộ khi hoàn thành. Espresso làm việc này đơn giản hơn nhiều, nó cũng giám sát hàng đợi sự kiện từ giao diện người dùng và tiếp tục chạy test của nó một khi không task nào đang chạy.

Nếu bạn sử dụng cá resources khác nhau, như 1 IntentService bạn cần implement 1 IdingResource. Sự thực thi này sẽ giám sát resource và đăng kí với Espresso Framework


import android.app.ActivityManager;
import android.content.Context;
import android.support.test.espresso.IdlingResource;

import java.util.List;

public class IntentServiceIdlingResource implements IdlingResource {

ResourceCallback resourceCallback;
 private Context context;

public IntentServiceIdlingResource(Context context) {
 this.context = context;
 }

@Override
 public String getName() {
 return IntentServiceIdlingResource.class.getName();
 }

@Override
 public void registerIdleTransitionCallback(
 ResourceCallback resourceCallback) {
 this.resourceCallback = resourceCallback;
 }

@Override
 public boolean isIdleNow() {
 boolean idle = !isIntentServiceRunning();
 if (idle &amp;amp;amp;amp;&amp;amp;amp;amp; resourceCallback != null) {
 resourceCallback.onTransitionToIdle();
 }
 return idle;
 }

private boolean isIntentServiceRunning() {
 ActivityManager manager =
 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
 // Get all running services
 List&amp;amp;amp;lt;ActivityManager.RunningServiceInfo&amp;amp;amp;gt; runningServices =
 manager.getRunningServices(Integer.MAX_VALUE);
 // check if our is running
 for (ActivityManager.RunningServiceInfo info : runningServices) {
 if (MyIntentService.class.getName().equals(
 info.service.getClassName())) {
 return true;
 }
 }
 return false;
 }
}


import android.app.Instrumentation;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.notNullValue;

@RunWith(AndroidJUnit4.class)
public class IntegrationTest {

 @Rule
 public ActivityTestRule rule = new ActivityTestRule(MainActivity.class);
 IntentServiceIdlingResource idlingResource;

 @Before
 public void before() {
 Instrumentation instrumentation
 = InstrumentationRegistry.getInstrumentation();
 Context ctx = instrumentation.getTargetContext();
 idlingResource = new IntentServiceIdlingResource(ctx);
 Espresso.registerIdlingResources(idlingResource);

 }
 @After
 public void after() {
 Espresso.unregisterIdlingResources(idlingResource);

 }

 @Test
 public void runSequence() {
 // this triggers our intent service, as we registered
 // Espresso for it, Espresso wait for it to finish
 onView(withId(R.id.action_settings)).perform(click());
 onView(withText("Broadcast")).check(matches(notNullValue()));
 }
}

9. Ứng dụng thứ 4: Kiểm thử asynchronous code với Espresso.

Tạo 1 project tên là Espresso Three, MainActivity gồm layout chứa 1 TextView, 1 Button

khi click vào button thì run asynch task. Chi tiết file MainActivity:


package com.bk.haodv.esspressothree;

import android.os.AsyncTask;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

Button btnStart;
TextView tvStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStart = (Button)findViewById(R.id.btn_start);
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tvStatus.setText("Running");
myTask.execute("test");
}
});
tvStatus = (TextView)findViewById(R.id.tv_status);
}
final AsyncTask<String, String, String> myTask = new AsyncTask<String, String, String>() {
@Override
protected String doInBackground(String... strings) {

for(int i = 1; i < 10; i++){
publishProgress("running... " + i) ;
SystemClock.sleep(500);
}
return "Long running stuff";
}

@Override
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);
tvStatus.setText(values[0]);
}

@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
tvStatus.setText("Done");
}
};
}

Code đẻ test MainActivity:


package com.bk.haodv.esspressothree;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

/**
* Created by T430 on 9/7/2016.
*/

@RunWith(AndroidJUnit4.class)
public class EspressoTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class);

@Test
public void btnShouldUpdateText(){
// click vào button start
onView(withId(R.id.btn_start)).perform(click());
// Espresso sẽ đợi đến khi task thực hiện xong, check status của Textview có chuẩn xác ko
onView(withId(R.id.tv_status)).check(matches(withText("Done")));
}
}

10.Ứng dụng 5: Tạo 1 custom Espresso matcher.

Android cung cấp class BoundedMatcher cho phép chúng ta tạo Espresso view matchers như 1 loại View riêng biệt.

Hãy viết 1 matcher , để kiểm thử text hints trên 1 Edittext.


public static Matcher<View> withItemHint(String itemHintText) {
checkArgument(!(itemHintText.equals(null)));
return withItemHint(is(itemHintText));
}

public static Matcher<View> withItemHint(final Matcher<String> matcherText) {
// use preconditions to fail fast when a test is creating an invalid matcher.
checkNotNull(matcherText);
return new BoundedMatcher<View, EditText>(EditText.class) {

@Override
public void describeTo(Description description) {
description.appendText("with item hint: " + matcherText);
}

@Override
protected boolean matchesSafely(EditText editTextField) {
return matcherText.matches(editTextField.getHint().toString());
}
};
}

Nó có thể được sử dụng như sau:


import static com.your.package.test.Matchers.withItemHint;
...
onView(withItemHint("test")).check(matches(isDisplayed()));

10. Nguồn tài liệu về Espresso

Android Espresso testing homepage

Git repository for the Android Testing Checkout the android-support-test branch for the testing libraries.

Sử dụng Retrofit HTTP Library trong Android

Chào các b, bài viết này mình muốn giới thiệu cách sử dụng Retrofit cho các ứng dụng liên quan đến http call, rest API, Retrofit được xây dựng bởi Square.

Retrofit được đánh giá khá cao bởi sự dễ dùng, hiệu năng cũng như sự mở rộng, so với các library khác như  Volley, Ksoap.. Trong ví dụ này ta sẽ xây dựng 1 app call HTTP request, hiện thông tin lên RecycleView dưới dạng list

Video Demo:

Ảnh ứng dụng ta sẽ xây dựng:

  1. Tạo Project

 

  1.  Tạo 1 project mới bằng cách click vào File => New Project.…chọn Empty Activty và next, next, next …:D
  2. Mở file build.gradle và add Retrofit, Gson dependencies.
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'

    // retrofit, gson
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
}

3. Cấp quyền truy cập INTERNET cho ứng dụng.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="haobk.dev4.mobile.retrofit">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
4. tạo các sub packages adapter, rest and model để triển khai code ứng dụng.

2.Tạo Model Class.

Đọc dữ liệu từ hộp đen taxi để xử lý tương tác trên Android

Chào các bạn, vừa rồi mình có làm 1 project khá hay (thực ra là 1 tính năng trong 1 project :D) cho 1 hãng taxi tại Việt Nam.

Như các bạn biết (Có thể chưa biết) trong mỗi xe taxi sẽ có đồng hồ (blackbox) và các cảm biến

Blackbox sẽ định kỳ gửi các bản tin lên server, trong các bản tin gửi lên sẽ có đầy đủ các thông tin như là: xe rỗng hay có khách, thời điểm lên xe, thời điểm xuống xe, số tiền khách phải thanh toán…
Việc tạo nên các message này là do đồng hồ và các cảm biến, ví dụ khi có ng mở cửa lên xe, các cảm biến sẽ nhận biết được và sau đó black box sẽ đẩy lên server bản tin khách lên xe chẳng hạn… Đồng hồ thì tính số km đã đi và số tiền thì dễ thấy rồi 😀

Chi tiết chức năng mình làm là, kết nối ứng dụng chạy trên tablet với blackbox thông qua 1 thiết bị trung gian, hiện tại tablet và thiết bị trung gian bên mình làm đều là từ hãng KingCom, thiết bị trung gian sẽ chuyển tiếp các bản tin từ blackbox và đồng hồ vào app của mình.
Có nghĩa là ngoài gửi lên server, blackbox cũng đẩy các message đó sang app mình, dựa vào các message đó, xử lý tương tác trên app thôi 😀

Tại sao mình phải làm chức năng này:
– Hạn chế thao tác cho tài (mình sẽ nói sau).

– Lấy số tiền và số km chính xác hoàn toàn (Khác với Uber đang có cơ chế ước lượng số tiền qua quãng đường đã đi và số phút nghỉ.