SlideShare a Scribd company logo
Android Test
Kyungho Jung
목차
Test 코드 작성해야 할까?
무엇부터 시작해야 할까?
뭘 쓰면 될까?
어떻게 할 까? 계산기 Step by 6step
Wrap-up
Q&A
발표의 목적
설득
지침 (Step by step)
Test 코드 작성해야 할까?
인간
Test 코드 작성해야 할까?
인간 = 실수를 끊임없이 함
인간 = 실수를 끊임없이 함
실수가 가장 무서울 때
젭알
가즈아
Test 코드 작성해야 할까?
Test 코드 작성해야 할까?
인간
Test 코드 작성해야 할까?
인간 = 실수를 끊임없이 함
Test 코드 작성해야 할까?
Test 코드 작성해야 할까?
빠른 피드백
안전성
실력향상
Test 코드 작성해야 할까?
네 짜야 합니다.
무엇부터 시작해야 할까?
UI Test 부터 시작합니다.
Acceptance test (인수 테스트) 라고 부르기도 합니다.
뭘 쓰면 될까?
Espresso
https://developer.android.com/training/testing/espresso/index.html
View 를 찾는다. 값을 확인하거나 행동을 한다
Firebase Test Lab for Android
테스트 코드를 잘 짜기 위해
테스트 코드를 잘 짜기 위해
테스트 코드를 잘 짜기 위해테스트 코드를 잘 짜기 위해
Database에 저장하는 것을 닌텐도에서 해야할까?
API 서버가 해야함.
게임 save 테스트 코드를 실행 할 때마다 API 서버 콜을 해야하나?
아님. 우리는 저장을 성공했다/실패했다를 가정해서 테스트 코드를 만들면 됨
계산기
테스트 코드를 잘 짜기 위해
계산기의 구조를 점점 개선해 나갈 거예요.
UI와 Calculate를 분리해서 아예 따로 테스트할 수 있게 하는게 목표예요.
Calculate가 변하더라도 테스트를 작성할 수 있게 만들거예요.
궁극적으로는 Calculate가 없어도 테스트는 성공하고 잘 돌아갈 거예요.
동영상 링크
계산기
1. UI 테스트
2. Unit 테스트
a. 4칙 연산 분리. Unit test 진행
3. Interface로 분리
a. Interface 로 나눠서 여러 행동을 정의할 수 있게 변경
4. Server와 함께 춤을
a. 동기/비동기 테스트
5. ViewModel도 함께 춤을
a. 계산 로직 분리
6. 나도 Dagger 좀 해보자
a. 테스트 모듈/프로덕션 모듈 분리
출력
숫자
결과
연산
자
findViewById(R.id.calculator_button_0).setOnClickListener(button -> setText("0"));
findViewById(R.id.calculator_button_1).setOnClickListener(button -> setText("1"));
...
findViewById(R.id.calculator_button_9).setOnClickListener(button -> setText("9"));
findViewById(R.id.calculator_button_plus).setOnClickListener(button -> setText("+"));
findViewById(R.id.calculator_button_minus).setOnClickListener(button -> setText("-"));
findViewById(R.id.calculator_button_multiply).setOnClickListener(button -> setText("*"));
findViewById(R.id.calculator_button_divide).setOnClickListener(button -> setText("/"));
findViewById(R.id.calculator_button_result).setOnClickListener(button -> calculate());
공통 부분 findView
Step1
UI 테스트
Step1 - Step1Activity.java calculate 함수
private void calculate() {
String expression = editText.getText().toString();
String symbol;
if (expression.contains("+")) {
symbol = "+";
}
...
List<String> arguments = Arrays.asList(expression.split(symbol));
int result;
switch (symbol) {
case "+":
result = Integer.valueOf(arguments.get(0)) + Integer.valueOf(arguments.get(1));
Break;
...
}
editText.setText(String.valueOf(result));
}
Espresso
https://developer.android.com/training/testing/espresso/index.html
View 를 찾는다. 값을 확인하거나 행동을 한다
Step1 - Step1ActivityTest.java
@Test public void plusTest() {
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_plus);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_result);
onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
}
@Test public void minusTest() { ... }
@Test public void multiplyTest() { ... }
@Test public void divideTest() { ... }
private void buttonClick(int id) {
onView(withId(id)).perform(click());
}
Step1
Test
동영상 링크
Step2
Unit 테스트
4칙 연산 분리. Unit test 진행
Step2 - Unit 테스트
Step1Activity
caclulate()
Step2 - Unit 테스트
Step2Activity
caclulate()
Calculator
Step2 - Unit 테스트
Step2Activity Calculator
caclulate()
Step2 - Unit 테스트
Calculator
caclulate()
1 눌러
+ 눌러
1 눌러
= 눌러
결과 확인해
Step2 - Step2Calculator.java
String calculate(String expression) {
String symbol;
if (expression.contains("+")) {symbol = "+";}
...
List<String> arguments = Arrays.asList(expression.split(symbol));
int result;
switch (symbol) {
case "+":
result = Integer.valueOf(arguments.get(0)) + Integer.valueOf(arguments.get(1));
break;
}
return String.valueOf(result);
}
Step2 - Step2UnitTest.java
public class Step2UnitTest {
private Step2Calculator calculator = new Step2Calculator();
@Test public void plusTest() {
String result = calculator.calculate("1+1");
assertThat(result).isEqualTo("2");
}
@Test public void minusTest() {
String result = calculator.calculate("1-1");
assertThat(result).isEqualTo("0");
}
...
}
Step2
JUnit
Test
동영상 링크
Step2
만약 여기까지 따라 오셨다면
Acceptance Test, Unit Test 를 작성한 것
Step3
Interface 로 분리
Interface 로 나눠서 여러 행동을 정의할 수 있게 변경
Step3 - Interface 로 분리
public interface Step3Calculator {
String calculate(String expression);
int plus(List<String> arguments);
int minus(List<String> arguments);
int multiply(List<String> arguments);
int divide(List<String> arguments);
}
Step4
Server와 함께 춤을
동기/비동기 테스트
Step4 동기 비동기 테스트
이런 상황 많을거예요
Signup, Signin, User 데이터를 서버에 저장.
Step4 동기 비동기 테스트
Stream (Rxjava)
Step4 - Step4Activity.java
public class Step4Activity extends AppCompatActivity {
private Calculator calculator = new Step4CalculatorImpl();
…
private void calculate() {
String expression = editText.getText().toString();
calculator.calculate(expression)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace);
}
}
Step3Activity.java
private void calculate() {
String expression = editText.getText().toString();
String result = calculator.calculate(expression);
editText.setText(result);
}
Step4 - Step4CalculatorImpl.java
public interface Calculator {
Single<Response<String>> calculate(String expression);
}
public class Step4CalculatorImpl implements Calculator {
public Single<Response<String>> calculate(String expression) {
Retrofit retrofit = ...
CalculatorService service = retrofit.create(CalculatorService.class);
return service.calculate(expression);
}
}
Step4 - Step4ActivityTest.java
public class Step4ActivityTest {
@Test public void plusTest() throws InterruptedException {
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_plus);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_result);
Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다.
onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
}
...
}
Step4
Test
동영상 링크
Step5
ViewModel도 함께 춤을
계산 로직을 분리
Step5 ViewModel
View에서 어떤 처리 로직을 분리
View와 어떤 처리 로직을 상관없게 만드는 것.
예) (여기서는) 계산. (실제 서비스에서는) Signup 같은
Step5 - Step4 와 Step5 비교
private void calculate() {
String expression = editText.getText().toString();
viewModel.calculate(expression);
}
public void onCreate(@Nullable Bundle savedInstanceState) {
... init codes
disposable.add(
((Step5ViewModel)viewModel).getHttpStream().observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace)
);
}
private void calculate() {
String expression = editText.getText().toString();
calculator.calculate(expression)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace);
}
Step4Activity.java
Step5Activity.java
Step5 - Step5ViewModel.java
public class Step5ViewModel implements ViewModel {
private Relay<Response<String>> httpStream = PublishRelay.create();
private Calculator calculator = new Step5CalculatorImpl();
@Override
public void calculate(String expression) {
calculator.calculate(expression)
.subscribe(result -> httpStream.accept(result), Throwable::printStackTrace);;
}
public Relay<Response<String>> getHttpStream() {
return httpStream;
}
}
Step4 - Step4ActivityTest.java
public class Step4ActivityTest {
@Test public void plusTest() throws InterruptedException {
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_plus);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_result);
Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다.
onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
}
...
}
Step5 - Step5ActivityTest.java
public class Step4ActivityTest {
@Test public void plusTest() throws InterruptedException {
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_plus);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_result);
Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다.
onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
}
...
}
TestObserver<Response<String>> testObserver = TestObserver.create();
((Step5ViewModel) testRule.getActivity().getViewModel()).getHttpStream()
.subscribe(testObserver);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_plus);
buttonClick(R.id.calculator_button_1);
buttonClick(R.id.calculator_button_result);
testObserver.awaitCount(1); <---- API 콜이 끝날 때, await가 풀립니다.
onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
Step5 Step4동영상 링크 동영상 링크
Step6
나도 Dagger 좀 해보자
테스트/프로덕션 모듈 분리
Step6 - Dependency Injection
Production Test
Activity
CalculatorModule CalculatorModule
Step6 - Dependency Injection
이럴 때 추천
일일이 new Someting() 하고 싶지 않을 때
Test와 Production 에서 사용하는 코드를 다르게 하고 싶을 때
Step6 - CalculatorApplication.java
public class CalculatorApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
step6Component = DaggerStep6Component.builder()
.step6Module(new Step6Module()) <---- 원래 모듈
.build();
}
...
}
Step6 - Step6Activity.java
public class Step6Activity extends AppCompatActivity {
@Inject Calculator calculator;
@Inject ViewModel viewModel;
@Inject Relay<Response<String>> httpStream;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
... init code
((CalculatorApplication) getApplication()).getStep6Component().inject(this);
}
}
Step6 - Step6ActivityTest.java
public class Step6ActivityTest {
@Inject Relay<Response<String>> httpStream;
@Before public void setUp() {
CalculatorApplication application = (CalculatorApplication) InstrumentationRegistry
.getTargetContext().getApplicationContext();
TestStep6Component testComponent = DaggerStep6TestComponent.builder()
.step6TestModule(new Step6TestModule())
.build();
application.setStep6Component(testComponent);
testComponent.inject(this);
}
}
App의 application 객체를 가져와서
module을 교체함.
Step6 - Step6ActivityTest.java
public class Step6ActivityTest {
@Inject Relay<Response<String>> httpStream;
@Before public void setUp() {
CalculatorApplication application = (CalculatorApplication) InstrumentationRegistry
.getTargetContext().getApplicationContext();
TestStep6Component testComponent = DaggerStep6TestComponent.builder()
.step6TestModule(new Step6TestModule()) <---- TestModule 로 교체
.build();
application.setStep6Component(testComponent);
testComponent.inject(this);
}
}
Step6 - Step6Module.java
public class Step6Module {
@Provides @Singleton
public Relay<Response<String>> provideHttpStream() {
return PublishRelay.create();
}
@Provides @Singleton
public Calculator provideCalculator() {
return new Step6CalculatorImpl();
}
@Provides @Singleton
public ViewModel provideViewModel(Calculator calculator, Relay<Response<String>> httpStream) {
return new Step6ViewModel(calculator, httpStream);
}
}
Step6 - Step6UnitTestModule.java
@Module
public class Step6UnitTestModule {
...
@Provides @Singleton
public Calculator provideCalculator() {
Calculator calculator = mock(Calculator.class);
when(calculator.calculate("1+1")).thenReturn(Single.just(Response.success("2")));
when(calculator.calculate("1-1")).thenReturn(Single.just(Response.success("0")));
when(calculator.calculate("3*2")).thenReturn(Single.just(Response.success("9")));
when(calculator.calculate("8/2")).thenReturn(Single.just(Response.success("4")));
return calculator;
}
...
}
Step6 Step5동영상 링크 동영상 링크
Wrap-up
사실 이제까지 한 이야기는
Design
Wrap-up
테스트 짜야합니다. 오늘부터 해봐요!
Test 짜기 어렵습니다. 뭘 해야할지 모를 때도 있어요. 그럴 땐, 눈에 잘 보이는 것
부터 해요. UI 테스트부터 짜봐요.
Dagger 써요. 좋습니다.
Q&A
Github
https://github.com/moltak/droidnights2018
Reference
https://developer.android.com/training/testing/espresso/index.html
https://github.com/tomoima525/DaggerTestExample

More Related Content

Similar to Droid knights android test @Droid Knights 2018

Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
beom kyun choi
 
Android UI Test (Espresso/Kakao)
Android UI Test (Espresso/Kakao)Android UI Test (Espresso/Kakao)
Android UI Test (Espresso/Kakao)
MDLicht
 
TDD&Refactoring Day 03: TDD
TDD&Refactoring Day 03: TDDTDD&Refactoring Day 03: TDD
TDD&Refactoring Day 03: TDDSuwon Chae
 
TEST?
TEST?TEST?
Design pattern 옵저버
Design pattern 옵저버Design pattern 옵저버
Design pattern 옵저버Sukjin Yun
 
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
NAVER D2
 
Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005
Ryan Park
 
The roadtocodecraft
The roadtocodecraftThe roadtocodecraft
The roadtocodecraftbbongcsu
 
Angular2 가기전 Type script소개
 Angular2 가기전 Type script소개 Angular2 가기전 Type script소개
Angular2 가기전 Type script소개
Dong Jun Kwon
 
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
Kenneth Ceyer
 
Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기
hanbeom Park
 
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It종빈 오
 
Agile Test Driven Development For Games What, Why, And How
Agile Test Driven Development For Games What, Why, And HowAgile Test Driven Development For Games What, Why, And How
Agile Test Driven Development For Games What, Why, And HowRyan Park
 
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
Kwangsung Ha
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기
우영 주
 
Spring Boot 2
Spring Boot 2Spring Boot 2
Spring Boot 2
경륜 이
 
그래서 테스트 코드는 어떻게 작성하나요?.pdf
그래서 테스트 코드는 어떻게 작성하나요?.pdf그래서 테스트 코드는 어떻게 작성하나요?.pdf
그래서 테스트 코드는 어떻게 작성하나요?.pdf
tokijh
 
Clean code
Clean codeClean code
Clean codebbongcsu
 
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
OKKY
 
MVP 패턴 소개
MVP 패턴 소개MVP 패턴 소개
MVP 패턴 소개
beom kyun choi
 

Similar to Droid knights android test @Droid Knights 2018 (20)

Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
 
Android UI Test (Espresso/Kakao)
Android UI Test (Espresso/Kakao)Android UI Test (Espresso/Kakao)
Android UI Test (Espresso/Kakao)
 
TDD&Refactoring Day 03: TDD
TDD&Refactoring Day 03: TDDTDD&Refactoring Day 03: TDD
TDD&Refactoring Day 03: TDD
 
TEST?
TEST?TEST?
TEST?
 
Design pattern 옵저버
Design pattern 옵저버Design pattern 옵저버
Design pattern 옵저버
 
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
 
Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005
 
The roadtocodecraft
The roadtocodecraftThe roadtocodecraft
The roadtocodecraft
 
Angular2 가기전 Type script소개
 Angular2 가기전 Type script소개 Angular2 가기전 Type script소개
Angular2 가기전 Type script소개
 
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
 
Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기
 
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It
[WELC] 22. I Need to Change a Monster Method and I Can’t Write Tests for It
 
Agile Test Driven Development For Games What, Why, And How
Agile Test Driven Development For Games What, Why, And HowAgile Test Driven Development For Games What, Why, And How
Agile Test Driven Development For Games What, Why, And How
 
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
[강연] 학생에서 현업 개발자로의 성공적인 변신을 위하여
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기
 
Spring Boot 2
Spring Boot 2Spring Boot 2
Spring Boot 2
 
그래서 테스트 코드는 어떻게 작성하나요?.pdf
그래서 테스트 코드는 어떻게 작성하나요?.pdf그래서 테스트 코드는 어떻게 작성하나요?.pdf
그래서 테스트 코드는 어떻게 작성하나요?.pdf
 
Clean code
Clean codeClean code
Clean code
 
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기
 
MVP 패턴 소개
MVP 패턴 소개MVP 패턴 소개
MVP 패턴 소개
 

More from KyungHo Jung

May 05 test_code_states
May 05 test_code_statesMay 05 test_code_states
May 05 test_code_states
KyungHo Jung
 
Rx Creating Operators, observeOn, subscribeOn
Rx Creating Operators, observeOn, subscribeOnRx Creating Operators, observeOn, subscribeOn
Rx Creating Operators, observeOn, subscribeOn
KyungHo Jung
 
Kotlin 사용기
Kotlin 사용기Kotlin 사용기
Kotlin 사용기
KyungHo Jung
 
Mockito, Robobinding
Mockito, RobobindingMockito, Robobinding
Mockito, Robobinding
KyungHo Jung
 
Philips Hue 공모전 - Color master
Philips Hue 공모전 - Color masterPhilips Hue 공모전 - Color master
Philips Hue 공모전 - Color master
KyungHo Jung
 
Weird camp proposal
Weird camp proposalWeird camp proposal
Weird camp proposalKyungHo Jung
 
Opensource java library deploy public repository
Opensource java library deploy public repositoryOpensource java library deploy public repository
Opensource java library deploy public repository
KyungHo Jung
 
Android MVVM TDD
Android MVVM TDDAndroid MVVM TDD
Android MVVM TDD
KyungHo Jung
 

More from KyungHo Jung (9)

May 05 test_code_states
May 05 test_code_statesMay 05 test_code_states
May 05 test_code_states
 
Rx Creating Operators, observeOn, subscribeOn
Rx Creating Operators, observeOn, subscribeOnRx Creating Operators, observeOn, subscribeOn
Rx Creating Operators, observeOn, subscribeOn
 
Kotlin 사용기
Kotlin 사용기Kotlin 사용기
Kotlin 사용기
 
Mockito, Robobinding
Mockito, RobobindingMockito, Robobinding
Mockito, Robobinding
 
Philips Hue 공모전 - Color master
Philips Hue 공모전 - Color masterPhilips Hue 공모전 - Color master
Philips Hue 공모전 - Color master
 
Weird camp proposal
Weird camp proposalWeird camp proposal
Weird camp proposal
 
Opensource java library deploy public repository
Opensource java library deploy public repositoryOpensource java library deploy public repository
Opensource java library deploy public repository
 
Android MVVM TDD
Android MVVM TDDAndroid MVVM TDD
Android MVVM TDD
 
Andoid ux, secure
Andoid ux, secureAndoid ux, secure
Andoid ux, secure
 

Droid knights android test @Droid Knights 2018

  • 2.
  • 3. 목차 Test 코드 작성해야 할까? 무엇부터 시작해야 할까? 뭘 쓰면 될까? 어떻게 할 까? 계산기 Step by 6step Wrap-up Q&A
  • 5. Test 코드 작성해야 할까? 인간
  • 6. Test 코드 작성해야 할까? 인간 = 실수를 끊임없이 함
  • 7. 인간 = 실수를 끊임없이 함
  • 11. Test 코드 작성해야 할까? 인간
  • 12. Test 코드 작성해야 할까? 인간 = 실수를 끊임없이 함
  • 14. Test 코드 작성해야 할까? 빠른 피드백 안전성 실력향상
  • 15. Test 코드 작성해야 할까? 네 짜야 합니다.
  • 16. 무엇부터 시작해야 할까? UI Test 부터 시작합니다. Acceptance test (인수 테스트) 라고 부르기도 합니다.
  • 19. Firebase Test Lab for Android
  • 20. 테스트 코드를 잘 짜기 위해
  • 21. 테스트 코드를 잘 짜기 위해
  • 22. 테스트 코드를 잘 짜기 위해테스트 코드를 잘 짜기 위해 Database에 저장하는 것을 닌텐도에서 해야할까? API 서버가 해야함. 게임 save 테스트 코드를 실행 할 때마다 API 서버 콜을 해야하나? 아님. 우리는 저장을 성공했다/실패했다를 가정해서 테스트 코드를 만들면 됨
  • 24. 테스트 코드를 잘 짜기 위해 계산기의 구조를 점점 개선해 나갈 거예요. UI와 Calculate를 분리해서 아예 따로 테스트할 수 있게 하는게 목표예요. Calculate가 변하더라도 테스트를 작성할 수 있게 만들거예요. 궁극적으로는 Calculate가 없어도 테스트는 성공하고 잘 돌아갈 거예요.
  • 26. 계산기 1. UI 테스트 2. Unit 테스트 a. 4칙 연산 분리. Unit test 진행 3. Interface로 분리 a. Interface 로 나눠서 여러 행동을 정의할 수 있게 변경 4. Server와 함께 춤을 a. 동기/비동기 테스트 5. ViewModel도 함께 춤을 a. 계산 로직 분리 6. 나도 Dagger 좀 해보자 a. 테스트 모듈/프로덕션 모듈 분리
  • 28. findViewById(R.id.calculator_button_0).setOnClickListener(button -> setText("0")); findViewById(R.id.calculator_button_1).setOnClickListener(button -> setText("1")); ... findViewById(R.id.calculator_button_9).setOnClickListener(button -> setText("9")); findViewById(R.id.calculator_button_plus).setOnClickListener(button -> setText("+")); findViewById(R.id.calculator_button_minus).setOnClickListener(button -> setText("-")); findViewById(R.id.calculator_button_multiply).setOnClickListener(button -> setText("*")); findViewById(R.id.calculator_button_divide).setOnClickListener(button -> setText("/")); findViewById(R.id.calculator_button_result).setOnClickListener(button -> calculate()); 공통 부분 findView
  • 30. Step1 - Step1Activity.java calculate 함수 private void calculate() { String expression = editText.getText().toString(); String symbol; if (expression.contains("+")) { symbol = "+"; } ... List<String> arguments = Arrays.asList(expression.split(symbol)); int result; switch (symbol) { case "+": result = Integer.valueOf(arguments.get(0)) + Integer.valueOf(arguments.get(1)); Break; ... } editText.setText(String.valueOf(result)); }
  • 32. Step1 - Step1ActivityTest.java @Test public void plusTest() { buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_plus); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_result); onView(withId(R.id.calculator_edit_text)).check(matches(withText("2"))); } @Test public void minusTest() { ... } @Test public void multiplyTest() { ... } @Test public void divideTest() { ... } private void buttonClick(int id) { onView(withId(id)).perform(click()); }
  • 34. Step2 Unit 테스트 4칙 연산 분리. Unit test 진행
  • 35. Step2 - Unit 테스트 Step1Activity caclulate()
  • 36. Step2 - Unit 테스트 Step2Activity caclulate() Calculator
  • 37. Step2 - Unit 테스트 Step2Activity Calculator caclulate()
  • 38. Step2 - Unit 테스트 Calculator caclulate() 1 눌러 + 눌러 1 눌러 = 눌러 결과 확인해
  • 39. Step2 - Step2Calculator.java String calculate(String expression) { String symbol; if (expression.contains("+")) {symbol = "+";} ... List<String> arguments = Arrays.asList(expression.split(symbol)); int result; switch (symbol) { case "+": result = Integer.valueOf(arguments.get(0)) + Integer.valueOf(arguments.get(1)); break; } return String.valueOf(result); }
  • 40. Step2 - Step2UnitTest.java public class Step2UnitTest { private Step2Calculator calculator = new Step2Calculator(); @Test public void plusTest() { String result = calculator.calculate("1+1"); assertThat(result).isEqualTo("2"); } @Test public void minusTest() { String result = calculator.calculate("1-1"); assertThat(result).isEqualTo("0"); } ... }
  • 42. Step2 만약 여기까지 따라 오셨다면 Acceptance Test, Unit Test 를 작성한 것
  • 43. Step3 Interface 로 분리 Interface 로 나눠서 여러 행동을 정의할 수 있게 변경
  • 44. Step3 - Interface 로 분리 public interface Step3Calculator { String calculate(String expression); int plus(List<String> arguments); int minus(List<String> arguments); int multiply(List<String> arguments); int divide(List<String> arguments); }
  • 46. Step4 동기 비동기 테스트 이런 상황 많을거예요 Signup, Signin, User 데이터를 서버에 저장.
  • 47. Step4 동기 비동기 테스트 Stream (Rxjava)
  • 48. Step4 - Step4Activity.java public class Step4Activity extends AppCompatActivity { private Calculator calculator = new Step4CalculatorImpl(); … private void calculate() { String expression = editText.getText().toString(); calculator.calculate(expression) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace); } } Step3Activity.java private void calculate() { String expression = editText.getText().toString(); String result = calculator.calculate(expression); editText.setText(result); }
  • 49. Step4 - Step4CalculatorImpl.java public interface Calculator { Single<Response<String>> calculate(String expression); } public class Step4CalculatorImpl implements Calculator { public Single<Response<String>> calculate(String expression) { Retrofit retrofit = ... CalculatorService service = retrofit.create(CalculatorService.class); return service.calculate(expression); } }
  • 50. Step4 - Step4ActivityTest.java public class Step4ActivityTest { @Test public void plusTest() throws InterruptedException { buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_plus); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_result); Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다. onView(withId(R.id.calculator_edit_text)).check(matches(withText("2"))); } ... }
  • 53. Step5 ViewModel View에서 어떤 처리 로직을 분리 View와 어떤 처리 로직을 상관없게 만드는 것. 예) (여기서는) 계산. (실제 서비스에서는) Signup 같은
  • 54. Step5 - Step4 와 Step5 비교 private void calculate() { String expression = editText.getText().toString(); viewModel.calculate(expression); } public void onCreate(@Nullable Bundle savedInstanceState) { ... init codes disposable.add( ((Step5ViewModel)viewModel).getHttpStream().observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace) ); } private void calculate() { String expression = editText.getText().toString(); calculator.calculate(expression) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> editText.setText(result.body()), Throwable::printStackTrace); } Step4Activity.java Step5Activity.java
  • 55. Step5 - Step5ViewModel.java public class Step5ViewModel implements ViewModel { private Relay<Response<String>> httpStream = PublishRelay.create(); private Calculator calculator = new Step5CalculatorImpl(); @Override public void calculate(String expression) { calculator.calculate(expression) .subscribe(result -> httpStream.accept(result), Throwable::printStackTrace);; } public Relay<Response<String>> getHttpStream() { return httpStream; } }
  • 56. Step4 - Step4ActivityTest.java public class Step4ActivityTest { @Test public void plusTest() throws InterruptedException { buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_plus); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_result); Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다. onView(withId(R.id.calculator_edit_text)).check(matches(withText("2"))); } ... }
  • 57. Step5 - Step5ActivityTest.java public class Step4ActivityTest { @Test public void plusTest() throws InterruptedException { buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_plus); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_result); Thread.sleep(2000); <---- calculate 가 언제 끝날지 모르지만 대충 2초 기다립니다. onView(withId(R.id.calculator_edit_text)).check(matches(withText("2"))); } ... } TestObserver<Response<String>> testObserver = TestObserver.create(); ((Step5ViewModel) testRule.getActivity().getViewModel()).getHttpStream() .subscribe(testObserver); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_plus); buttonClick(R.id.calculator_button_1); buttonClick(R.id.calculator_button_result); testObserver.awaitCount(1); <---- API 콜이 끝날 때, await가 풀립니다. onView(withId(R.id.calculator_edit_text)).check(matches(withText("2")));
  • 58. Step5 Step4동영상 링크 동영상 링크
  • 59. Step6 나도 Dagger 좀 해보자 테스트/프로덕션 모듈 분리
  • 60. Step6 - Dependency Injection Production Test Activity CalculatorModule CalculatorModule
  • 61. Step6 - Dependency Injection 이럴 때 추천 일일이 new Someting() 하고 싶지 않을 때 Test와 Production 에서 사용하는 코드를 다르게 하고 싶을 때
  • 62. Step6 - CalculatorApplication.java public class CalculatorApplication extends Application { @Override public void onCreate() { super.onCreate(); step6Component = DaggerStep6Component.builder() .step6Module(new Step6Module()) <---- 원래 모듈 .build(); } ... }
  • 63. Step6 - Step6Activity.java public class Step6Activity extends AppCompatActivity { @Inject Calculator calculator; @Inject ViewModel viewModel; @Inject Relay<Response<String>> httpStream; @Override public void onCreate(@Nullable Bundle savedInstanceState) { ... init code ((CalculatorApplication) getApplication()).getStep6Component().inject(this); } }
  • 64. Step6 - Step6ActivityTest.java public class Step6ActivityTest { @Inject Relay<Response<String>> httpStream; @Before public void setUp() { CalculatorApplication application = (CalculatorApplication) InstrumentationRegistry .getTargetContext().getApplicationContext(); TestStep6Component testComponent = DaggerStep6TestComponent.builder() .step6TestModule(new Step6TestModule()) .build(); application.setStep6Component(testComponent); testComponent.inject(this); } } App의 application 객체를 가져와서 module을 교체함.
  • 65. Step6 - Step6ActivityTest.java public class Step6ActivityTest { @Inject Relay<Response<String>> httpStream; @Before public void setUp() { CalculatorApplication application = (CalculatorApplication) InstrumentationRegistry .getTargetContext().getApplicationContext(); TestStep6Component testComponent = DaggerStep6TestComponent.builder() .step6TestModule(new Step6TestModule()) <---- TestModule 로 교체 .build(); application.setStep6Component(testComponent); testComponent.inject(this); } }
  • 66. Step6 - Step6Module.java public class Step6Module { @Provides @Singleton public Relay<Response<String>> provideHttpStream() { return PublishRelay.create(); } @Provides @Singleton public Calculator provideCalculator() { return new Step6CalculatorImpl(); } @Provides @Singleton public ViewModel provideViewModel(Calculator calculator, Relay<Response<String>> httpStream) { return new Step6ViewModel(calculator, httpStream); } }
  • 67. Step6 - Step6UnitTestModule.java @Module public class Step6UnitTestModule { ... @Provides @Singleton public Calculator provideCalculator() { Calculator calculator = mock(Calculator.class); when(calculator.calculate("1+1")).thenReturn(Single.just(Response.success("2"))); when(calculator.calculate("1-1")).thenReturn(Single.just(Response.success("0"))); when(calculator.calculate("3*2")).thenReturn(Single.just(Response.success("9"))); when(calculator.calculate("8/2")).thenReturn(Single.just(Response.success("4"))); return calculator; } ... }
  • 68. Step6 Step5동영상 링크 동영상 링크
  • 70. 사실 이제까지 한 이야기는 Design
  • 71. Wrap-up 테스트 짜야합니다. 오늘부터 해봐요! Test 짜기 어렵습니다. 뭘 해야할지 모를 때도 있어요. 그럴 땐, 눈에 잘 보이는 것 부터 해요. UI 테스트부터 짜봐요. Dagger 써요. 좋습니다.
  • 72. Q&A

Editor's Notes

  1. 안녕하세요. Android Test 발표를 하게된 정경호입니다. 제 발표를 들으러 와주셔서 너무 감사하구요, 발표 내용이 많은데 나중에 github 과 presentation 동영상이 공유가 될테니 제 발표를 듣고 테스트를 작성하고 싶으신 분은 그 자료들을 참고해주시면 좋을거 같습니다.
  2. 발표 설득 대상은 Unit Test 만, QA, 언젠간, 내 코드는
  3. 제 발표의 목차는 이렇게 되어있구요. Step by step 에서 6단계의 앱 개발 단계가 있습니다.
  4. 언젠가 발표 자료와 github 코드를 보고 적용하도록 하는 것.
  5. Test 코드 짜야할까? 저는 여기에 대한 답으로 이 단어를 지정했습니다. 여기 제가 있습니다. 저는 인간입니다. 인간은 (다음 슬라이드로 넘기며) 실수를 아주 많이합니다.
  6. 실수를 아주 많이 합니다. 사실은 정말 끊임없이 합니다.
  7. 밥먹듯이 실수를 합니다. 인간은 불완전한 존재이기 때문에 실수를 하고 실수를 통해 성장하기도 합니다. 하지만 실수가 항상 좋지만은 않죠? 어떤 날은 너무 많이 해서 제 자신에게 짜증이 날 때도 있죠. 특히 이런 실수는 언제 많이 나오냐. 중요한 순간에 자주 눈에 띕니다. 다른때는 별로 신경안써도 되는 일이 중요한 순간에는 눈에 너무 잘 띄죠.
  8. 제 생각엔 배포 직후인것 같습니다. 시험기간에 뭔가 여기볼까 말까 했던 곳에서 막 시험문제 나왔던 것처럼, 배포하고 나면 뭔가 아 이거 빼먹었다, 아 버그 발견! 이런 경우가 많았던것 같아요.
  9. 그래서 항상 이런 상태가 되는 것 같습니다
  10. 저는 스타트업에 다니고 있습니다. 이름은 에그번이예요. 챗봇기반 언어 학습 앱입니다. 한국어, 중국어, 일어를 영어, 프랑스어, 스페인어 등으로 가르쳐요. 혹시 한국어가 서툴다 하시는 분은 저희 서비스 써보세요. 도움 될겁니다. 어쨌든 저희도 제품을 개발하고 배포할때, 무언가 붙잡고 싶고 기도하고 싶고 합니다. 왜냐면 스타트업은 일이 너무 많기 때문에 충분할만큼 손으로 테스트하지 못하기 때문이죠.
  11. 그럼 다시 처음으로 돌아저희는 열심히 테스트를 짜지만 인간이기 떄문에
  12. 인간은 실수를 끊임없이 하기 때문에 테스트 코드가 도움이 됩니다.
  13. 박스: 실수를 하지 말자/실수를 빨리 알아차리자 테스트 코드는 확성기와 비슷합니다. 문제가 있으면 빠르게 알아차릴 수 있도록 큰소리로 알려 줍니다. CI 를 이용한다면 슬랙을 통해서 모든 직원에게 동시에 알림을 줄 수도 있겠죠. 손으로 누를 필요없이 컴퓨터가 스스로 실행하기 때문에 피드백을 더 빨리 받을 수 있습니다.
  14. 네 저는 인간이라 실수를 안할 수가 없습니다. 그래서 실수를 빨리 알아차리는게 중요하다고 생각했습니다. 만약 여러분도 저와 비슷한 마음이시라면 여기 잘 들어 오셨습니다. 실수 안한다 하시는 분은 옆에 있는 방으로 넘어가서 그거 들으시면 됩니다. 두 번째는 실력 향상입니다. 테스트를 잘 짜긴 위해선 기본기가 중요하게 됩니다. 그 기본기는 바로 desgin 입니다. OOP 에 대한 지식과 경험이 있어야 제대로 test 코드를 짤 수 있고 곧 이는 더 나은 코드 더 확장성이 나은 코드를 만드는데 도움이 됩니다. 그래서 저는
  15. UI Test 부터 합니다.
  16. 안드로이드에는 Espresso 라는 test framework 가 존재합니다. 이 프레임워크는 Acceptance Test (인수 테스트, functional test)를 지원합니다. 그냥 처음에 이렇게 시작하는 겁니다. UI 있죠? Button 누르면 어떤 행동이 실행되거나 출력값이 나와야합니다. 그걸 먼저 만듭니다. 숲을 먼저 보는 봅니다. Unit 테스트로는 전체 앱 flow 를 그리는 것에 한계가 뚜렷합니다. Unit 이라는 단어 자체가 아주 작은 것을 의미하고 있기 때문에 많은 일을 하면 안되죠.
  17. 안드로이드에는 Espresso 라는 test framework 가 존재합니다. 이 프레임워크는 Acceptance Test (인수 테스트, functional test)를 지원합니다. 그냥 처음에 이렇게 시작하는 겁니다. UI 있죠? Button 누르면 어떤 행동이 실행되거나 출력값이 나와야합니다. 그걸 먼저 만듭니다. 숲을 먼저 보는 봅니다. Unit 테스트로는 전체 앱 flow 를 그리는 것에 한계가 뚜렷합니다. Unit 이라는 단어 자체가 아주 작은 것을 의미하고 있기 때문에 많은 일을 하면 안되죠.
  18. 안드로이드에는 Espresso 라는 test framework 가 존재합니다. 이 프레임워크는 Acceptance Test (인수 테스트, functional test)를 지원합니다. 그냥 처음에 이렇게 시작하는 겁니다. UI 있죠? Button 누르면 어떤 행동이 실행되거나 출력값이 나와야합니다. 그걸 먼저 만듭니다. 숲을 먼저 보는 봅니다. Unit 테스트로는 전체 앱 flow 를 그리는 것에 한계가 뚜렷합니다. Unit 이라는 단어 자체가 아주 작은 것을 의미하고 있기 때문에 많은 일을 하면 안되죠.
  19. 아 이거 뭐였지 다음 페이지에 넘어가기 전에 뜸들이는?
  20. 이런 어려운 걸 잡는다고 해보죠. 잡았어요 힘들게. 위에 피 보면 40밖에 안남았어요. 원래 7500이에요. 잡은 후에 저장을 했어요. 저장은 서버가 하게 되요. 근데 우리가 테스트 코드에 저장했는지 안했는지 알아야할까요? 우리는 저장을 한다는 API 호출 하고 저장이 잘 됐다는 결과를 받으면 되요. 서버팀에서 사용자 데이터를 종이에 적든, 손에 적든, 기억해놓든 알게 뭡니까. 훌륭하신 분이니깐 잘 저장했겠지 믿는 겁니다.
  21. 이런 어려운 걸 잡는다고 해보죠. 잡았어요 힘들게. 위에 피 보면 40밖에 안남았어요. 원래 7500이에요. 잡은 후에 저장을 했어요. 저장은 서버가 하게 되요. 근데 우리가 테스트 코드에 저장했는지 안했는지 알아야할까요? 우리는 저장을 한다는 API 호출 하고 저장이 잘 됐다는 결과를 받으면 되요. 서버팀에서 사용자 데이터를 종이에 적든, 손에 적든, 기억해놓든 알게 뭡니까. 훌륭하신 분이니깐 잘 저장했겠지 믿는 겁니다.
  22. 우리도 비슷하게 만들어 볼거예요
  23. Calulate 가 아까 젤다에서는 저장이예요.
  24. 앱 실행을 보면 이렇게 생겼어요. 더하기 빼기 곱하기 나누기가 되요.
  25. 이렇게 설명할 거예요. 한번 발표하고 관심이 생기시면 제 github 프로젝트를 보시면서 따라해보시면 좋을거 같아요. 제가 주석도 조금 달아놨으니 도움이 될거예요.
  26. 전체 구조는 이렇구요.
  27. calculate 함수 설명할 것.
  28. 안드로이드에는 Espresso 라는 test framework 가 존재합니다. 이 프레임워크는 Acceptance Test (인수 테스트, functional test)를 지원합니다. 그냥 처음에 이렇게 시작하는 겁니다. UI 있죠? Button 누르면 어떤 행동이 실행되거나 출력값이 나와야합니다. 그걸 먼저 만듭니다. 숲을 먼저 보는 봅니다. Unit 테스트로는 전체 앱 flow 를 그리는 것에 한계가 뚜렷합니다. Unit 이라는 단어 자체가 아주 작은 것을 의미하고 있기 때문에 많은 일을 하면 안되죠.
  29. buttonClick 설명 Matches 설명
  30. Step1Activity가 있습니다.
  31. 따로 떼어낼거예요. 테스트를 더 원활히 하기 위해.
  32. 이렇게 Calculator 클래스를 만들어서 함수를 옆으로 넘겼어요.
  33. 그럼 우리는 UnitTest 를 짤 수 있게 되요. UI 없이 핵심로직, calculate 만 테스트할 수 있어요.
  34. calculate 함수는 똑같구요
  35. 이젠 unit test 를 이렇게 작성할 수 있어요. 아깐 4칙연산 테스트를 위해 UI 가 필요했는데 이젠 Calculator 만 필요해요.
  36. 훨씬 빠름. 제가 커서 옮기는 것 보다 더 빨라요.
  37. 이건 뭐 별거 없음
  38. 우리가 앱을 만들 때, 대체로 API server와 통신을 하는 경우가 많아요. 대체로 하죠. 그걸 가정하기 위해 Caclculate 함수를 서버에 넘겨 버렸어요. 나중에 깃헙에서 확인할 수 있구요.
  39. 요론 상황들을 테스트 하기 위함입니다. Step4는
  40. 그걸 위해 Stream 을 만들건데 Rxjava를 이용할 거예요.
  41. 파란 박스에 보면 Step3Activity 빨간 박스는 변경 부분
  42. Calculator 의 구현체는 이렇게
  43. 테스트 코드를 짜는데, 빨간색 박스 부분을 보죠. 중요합니다. 서버가 미국에 있을 수도 우주에 있을 수도 있고 서버팀이 막 더해줄수도 있어요. 우리는 실제로 서버에서 언제 응답이 올지 몰라요. 그쵸? 그래서 대충 2초 기다리게 했어요. 일단 빨리 다음으로 가봅시다.
  44. 이렇게 테스트를 실행하면 실제로는 테스트가 실패해버려요. 왜냐면 서버 응답이 오기전에 2초가 지났거든요. 이 다음 스텝에서 이걸 해결할거예요.
  45. ViewModel 을 통해 해결해보죠
  46. 위에 코드와 비교 아래 calculate 에서 viewModel.calculate() 설명
  47. 코드 잠깐 설명
  48. 여기 잠깐 설명. 다음 장에서 Step4와 Step5 비교
  49. testObserver.awaitCount(1) 설명
  50. Application 객체에서 component와 module 구성
  51. Activity에서 component 에 inject
  52. 하지만 test code 에선 이런식으로 사용
  53. 여기서 다시 테스트용 컴포넌트를 구성
  54. 실제 모듈은 이렇게 생겼지만
  55. 테스트 모듈은 어떤 요청이 왔을 때, 미리 지정된 결과를 전달하게 만들었음
  56. mocking 코드 보여주기