Clean Architecture & TDD · 2017. 9. 20. · • Android App Developer • Unit Test Enthusiast...

Preview:

Citation preview

Clean Architecture & TDD@fushiroyama

About Me

• Fumihiko Shiroyama

• Android App Developer

• Unit Test Enthusiast

• https://github.com/srym

Clean Architecture

• Presentation

• Domain

• Infrastructure

TDD

• Test Driven Development

• Test First

• Minimum Implementation

• Refactoring

TDD is great! because...

• Focus on I/O

• Less reworking

• Force Unit Testing

Example

• Infrastructure

• Remote Data Source

• GitHub Information

• Local Unit Test

Interface

public interface RemoteGitHubDataSource { Single<List<Repo>> listRepos(@NonNull String user);}

Blank Implementation

public class RestGitHubDataSource implements RemoteGitHubDataSource { @Override public Single<List<Repo>> listRepos(@NonNull String user) { return Single.error(new RuntimeException()); }}

Blank Implementation

public class RestGitHubDataSource implements RemoteGitHubDataSource { @Override public Single<List<Repo>> listRepos(@NonNull String user) { return Single.error(new RuntimeException()); }}

Create Test

• Mouse over class

• Alt + Enter

• Create Test

Create Test

Create Test

By the way...

• Mouse over class

• Shift + Command + T

• Choose Test

Test First

public class RestGitHubDataSourceTest { private RestGitHubDataSource dataSource;

@Before public void setUp() throws Exception { dataSource = new RestGitHubDataSource(); }

@Test public void listRepos() throws Exception { // implement here! }}

Test First

@Testpublic void listRepos() throws Exception { List<Repo> repos = restGitHubDataSource.listRepos("srym") .test() .await() .assertNoErrors() .assertComplete() .values() .get(0); assertThat(repos).isNotNull();}

Test First

assertThat(repos).isNotNull().isNotEmpty();Repo repo = repos.get(0);assertThat(repo).isNotNull();assertThat(repo.getFullName()).isNotBlank();assertThat(repo.getId()).isGreaterThanOrEqualTo(0);assertThat(repo.getOwner()).isNotNull();

Test First

assertThat(repos).isNotNull().isNotEmpty();Repo repo = repos.get(0);assertThat(repo).isNotNull();assertThat(repo.getFullName()).isNotBlank();assertThat(repo.getId()).isGreaterThanOrEqualTo(0);assertThat(repo.getOwner()).isNotNull();

You can confirm the specs BEFORE you implement

Test Execution (failure)

• This of course fails.

Minimum Implementation

public class RestGitHubDataSource implements RemoteGitHubDataSource { private final GitHubService gitHubService;

@Inject public RestGitHubDataSource(GitHubService gitHubService) { this.gitHubService = gitHubService; }

@Override public Single<List<Repo>> listRepos(@NonNull String user) { return gitHubService.listRepos(user); }}

Minimum Implementation

public class RestGitHubDataSource implements RemoteGitHubDataSource { private final GitHubService gitHubService;

@Inject public RestGitHubDataSource(GitHubService gitHubService) { this.gitHubService = gitHubService; }

@Override public Single<List<Repo>> listRepos(@NonNull String user) { return gitHubService.listRepos(user); }}

Minimum Implementation

public class RestGitHubDataSource implements RemoteGitHubDataSource { private final GitHubService gitHubService;

@Inject public RestGitHubDataSource(GitHubService gitHubService) { this.gitHubService = gitHubService; }

@Override public Single<List<Repo>> listRepos(@NonNull String user) { return gitHubService.listRepos(user); }}

But wait...

• This is Local Unit Test

• Mock data? Hmm.

MockWebServer

MockWebServer

• Provided by OkHttp

• Full HTTP Stack

• Can test REAL response

MockWebServer

private final MockWebServer mockWebServer = new MockWebServer();

MockWebServer

Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { return new MockResponse().setResponseCode(404); }};mockWebServer.setDispatcher(dispatcher);mockWebServer.start();

MockWebServer

@Overridepublic MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request == null || request.getPath() == null) { return new MockResponse().setResponseCode(400); }

if (request.getPath().matches("/users/.+/repos")) { return new MockResponse() .setBody(readJsonFromResources("users_repos.json")) .setResponseCode(200); }

return new MockResponse().setResponseCode(404);}

MockWebServer

@Overridepublic MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request == null || request.getPath() == null) { return new MockResponse().setResponseCode(400); }

if (request.getPath().matches("/users/.+/repos")) { return new MockResponse() .setBody(readJsonFromResources("users_repos.json")) .setResponseCode(200); }

return new MockResponse().setResponseCode(404);}

MockWebServer

@Overridepublic MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request == null || request.getPath() == null) { return new MockResponse().setResponseCode(400); }

if (request.getPath().matches("/users/.+/repos")) { return new MockResponse() .setBody(readJsonFromResources("users_repos.json")) .setResponseCode(200); }

return new MockResponse().setResponseCode(404);}

MockWebServer

@Overridepublic MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request == null || request.getPath() == null) { return new MockResponse().setResponseCode(400); }

if (request.getPath().matches("/users/.+/repos")) { return new MockResponse() .setBody(readJsonFromResources("users_repos.json")) .setResponseCode(200); }

return new MockResponse().setResponseCode(404);}

Talk about this later

MockWebServer

Retrofit retrofit = new Retrofit.Builder() .baseUrl(mockWebServer.url("")) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();GitHubService gitHubService = retrofit.create(GitHubService.class);

MockWebServer

Retrofit retrofit = new Retrofit.Builder() .baseUrl(mockWebServer.url("")) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();GitHubService gitHubService = retrofit.create(GitHubService.class);

Prepare Data

• Curl

• Postman

curl https://api.github.com/users/srym/repos > users_repos.json

Prepare Data

• Put it test/resources

Read JSON from file

private String readJsonFromResources(@NonNull String fileName) { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder stringBuilder = new StringBuilder(); try { String buffer; while ((buffer = bufferedReader.readLine()) != null) { stringBuilder.append(buffer); } } catch (IOException e) { fail(e.getMessage(), e); } return stringBuilder.toString();}

Fix Test

@Before public void setUp() throws Exception { // abbr. dataSource = new RestGitHubDataSource(gitHubService); }

@Test public void listRepos() throws Exception { List<Repo> repos = restGitHubDataSource.listRepos("srym") .test() .await() .assertNoErrors() .assertComplete() .values() .get(0); assertThat(repos).isNotNull(); // abbr. }

Passed!

Refactoring

• TDD is NOT perfect

• Repeat Write & Test

Cons

• Useless when API changes

• Takes longer time

Pros

• Quality

• Relief

• Takes shorter time in total

TDD rocks!

Thank You

Links

• https://github.com/srym/Architecture

• https://github.com/square/okhttp/tree/master/mockwebserver

• http://www.irasutoya.com/

Recommended