Presenter より下のレイヤについて
unit test の対象となるレイヤについても一応の確認をしておきます。ここより先はドキュメントとしての unit test を確認したほうが良いと思いますが、構造的な部分について注力して確認をすすめておく形ですすめます。
以下の順で確認を行います
- UseCase、GetRepositories - 呼び出し元は Presenter
- GitHubRepository、GitHubInfraRepository
- GitHubRemoteDatasource、GitHubLocalDataSource
UseCase あたりは domain 配下でそれより下は infrastructure 配下にて定義されています。順に確認をすすめます。
UseCase
以下な形で RepoListPresenter
にて使用されています。
@Override
public void getRepositoryList(@NonNull String account) {
view.showProgressBar();
ticket = getRepositories.enqueue(GetRepositories.Param.newInstance(account));
}
@Override
public void refreshRepositoryList(@NonNull String account) {
ticket = getRepositories.enqueue(GetRepositories.Param.newInstance(account, true));
}
先ず、基底クラスの UseCase
から確認します。定義が以下。
public abstract class UseCase<PARAM> {
@NonNull
private final TaskQueue taskQueue;
@NonNull
protected final Bus bus;
protected UseCase(@NonNull TaskQueue taskQueue, @NonNull Bus bus) {
this.taskQueue = taskQueue;
this.bus = bus;
}
@NonNull
public TaskTicket enqueue(PARAM param) {
Task task = buildTask(param);
Future<?> future = taskQueue.enqueue(task);
return new TaskTicket(task.getId(), future);
}
protected abstract Task buildTask(PARAM param);
}
インターフェースを提供して buildTask
の実装はサブクラスに、という形です。GetRepositories
の定義を順に確認します。
public class GetRepositories extends UseCase<GetRepositories.Param> {
@NonNull
private final GitHubRepository gitHubRepository;
@NonNull
private final RepositoryMapper repositoryMapper;
public GetRepositories(@NonNull TaskQueue taskQueue, @NonNull Bus bus, @NonNull GitHubRepository gitHubRepository, @NonNull RepositoryMapper repositoryMapper) {
super(taskQueue, bus);
this.gitHubRepository = gitHubRepository;
this.repositoryMapper = repositoryMapper;
}
@NonNull な属性二点 (GitHubRepository 型、RepositoryMapper 型) とコンストラクタの定義です。RepositoryMapper
については別途確認の方向です。
次に定義されているのは buildTask
メソッドです。定義を以下に引用します。
@Override
protected Task buildTask(Param param) {
return new Task() {
@Override
public void run() {
try {
List<RepositoryEntity> entities = param.refresh
? gitHubRepository.refreshUserRepositories(param.account)
: gitHubRepository.getUserRepositories(param.account);
List<Repository> repositories = repositoryMapper.convertList(entities);
bus.post(new OnSuccessGetRepositories(repositories));
} catch (ApiException e) {
GitHubErrorEntity gitHubErrorEntity = e.getGitHubErrorEntity();
String message = gitHubErrorEntity == null
? e.getMessage()
: gitHubErrorEntity.getMessage();
bus.post(new OnFailureGetRepositories(new Exception(message)));
} catch (Exception e) {
bus.post(new OnFailureGetRepositories(e));
}
}
};
}
基本的には param.refresh
の値によって
GitHubRepository#refreshUserRepositories
GitHubRepository#getUserRepositories
のどちらかを呼び出してその戻りを entities に格納し、RepositoryMapper#convertList
により List
- OnSuccessGetRepositories
- OnFailureGetRepositories
RepoListPresenter
にて定義されています (引用は略します)。また、このクラス定義ファイルにおいては
- Factory クラス
- PARAM の定義
の記述がありますが、詳細は略します。また、GetRepositories
の試験において確認されている事項を以下に列挙しておきます。
GetRepositories#enqueue
の呼び出し時、TaskQueue#enqueue
が一度呼び出されていることの確認GetRepositories.Param.newInstance("srym")
により作成したオブジェクトを使ってGetRepositories#buildTask().run()
した時に依存しているライブラリ手続きが呼び出されていることの確認 (詳細略)- エラーとなる状態を作った上で
GetRepositories#buildTask().run()
を呼び出して想定しているライブラリ手続きが呼び出されていることの確認 - buildTask に渡すパラメータの refresh が true の場合に想定しているライブラリ手続きが呼び出されていることの確認
- buildTask に渡すパラメータの refresh が true の場合のエラー処理において想定しているライブラリ手続きが呼び出されていることの確認
Repository
interface は datasource/GitHubRepository
で定義が以下です。
public interface GitHubRepository {
@NonNull
List<RepositoryEntity> getUserRepositories(@NonNull String account) throws IOException, ApiException;
List<RepositoryEntity> refreshUserRepositories(@NonNull String account) throws IOException, ApiException;
}
これを実装しているのが datasource/GitHubInfraRepository
になります。実装の引用は略します。属性は以下二点で @NonNull です。
- private final GitHubRemoteDataSource remoteDataSource
- private final GitHubLocalDataSource localDataSource
これらは両方コンストラクタにて値が設定されます。また、以降にて定義されているのは @Override な以下二点のメソッドと
- public List
getUserRepositories(@NonNull String account) - public List
refreshUserRepositories(@NonNull String account)
プライベートな変換メソッド二点と
- private RoomRepositoryEntity entityToRoom(RepositoryEntity)
- private RoomAccountEntity entityRoom(AccountEntity)
Factory クラスとなっています。singleton クラスというわけではなく、生成要求があれば都度オブジェクトを生成する形に見えます。また、このクラスの試験において確認されている事項を以下に列挙しておきます。
- ローカルデータソースにデータが存在する場合のテスト、この場合リモートデータソースの呼び出しは行われないことも検証
- ローカルデータソースもリモートデータソースも空の場合のテスト、呼び出されないメソッドなどの確認なども行う
- ローカルデータソースは空でリモートデータソースが結果を戻す場合のテスト、リモートデータソース取得時にはローカルデータソースへの書き込みも行われるため、その検証も行う
- リモートデータソースが空を戻す場合のテスト
- リモートデータソースが値を戻す場合のテスト
Datasource
先ず、datasource/local/
側から確認をすすめていきます。GitHubLocalDataSource
は interface 定義となっています。以下に引用します。
public interface GitHubLocalDataSource {
List<RepositoryEntity> getUserRepositories(@NonNull String account);
void insertRepositoriesAndAccounts(@NonNull List<RoomRepositoryEntity> repositories, @NonNull List<RoomAccountEntity> accounts);
void deleteAndInsertRepositoriesAndAccounts(@NonNull String account, @NonNull List<RoomRepositoryEntity> repositories, @NonNull List<RoomAccountEntity> accounts);
}
この interaface を実装しているのが GitHubRoomDataSource
です。属性は @NonNull でコンストラクタにて設定されています。
public class GitHubRoomDataSource implements GitHubLocalDataSource {
@NonNull
private final AppDatabase db;
@NonNull
private final RepositoryEntityMapper repositoryEntityMapper;
public GitHubRoomDataSource(@NonNull AppDatabase db, @NonNull RepositoryEntityMapper repositoryEntityMapper) {
this.db = db;
this.repositoryEntityMapper = repositoryEntityMapper;
}
以降は interface で実装を強制されているメソッドの定義となります。
@Override
public List<RepositoryEntity> getUserRepositories(@NonNull String account) {
List<RepoWithAccount> repoWithAccountList = db.repositoryDao().findByAccount(account);
return repositoryEntityMapper.convertList(repoWithAccountList);
}
@Override
public void insertRepositoriesAndAccounts(@NonNull List<RoomRepositoryEntity> repositories, @NonNull List<RoomAccountEntity> accounts) {
db.repositoryDao().insertRepositoriesAndAccounts(repositories, Stream.of(accounts).distinct().toList());
}
@Override
public void deleteAndInsertRepositoriesAndAccounts(@NonNull String account, @NonNull List<RoomRepositoryEntity> repositories, @NonNull List<RoomAccountEntity> accounts) {
db.repositoryDao().deleteAndInsertRepositoriesAndAccounts(account, repositories, Stream.of(accounts).distinct().toList());
}
}
以下に機能と用途を列挙します。基本的にデータのやりとりが発生するのはローカルにある SQLite となります。
- getUserRepositories はデータを取得して RepositoryEntity のリストに変換して戻します
- insertRepositoriesAndAccounts は Repositories および Accounts というテーブルにデータを insert する処理
- deleteAndInsertRepositoriesAndAccounts は指定されたデータを一旦消去して再度 insert する手続きとなります
ここで Room に関するざっくりとした確認をしておきます。先ず room/db/AppDataBase
にて dao と言うかたちでデータベースに存在するテーブルの定義をしておきます。
AppDatabase.java
@Database(entities = {RoomAccountEntity.class, RoomRepositoryEntity.class}, version = 1, exportSchema = false)
@TypeConverters({DateConverter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract RoomAccountDao accountDao();
public abstract RoomRepositoryDao repositoryDao();
}
dao ではデータ操作の方法を以下のような形で規定します。
@Dao
public abstract class RoomRepositoryDao {
@Query("SELECT * FROM repository")
public abstract List<RoomRepositoryEntity> getAll();
あるいは insertRepositoriesAndAccounts
の定義は以下のようになっています。
@Transaction
public void insertRepositoriesAndAccounts(List<RoomRepositoryEntity> repositories, List<RoomAccountEntity> accounts) {
insertAllRepositories(repositories.toArray(new RoomRepositoryEntity[repositories.size()]));
insertAllAccounts(accounts.toArray(new RoomAccountEntity[accounts.size()]));
}
呼び出されている二点の手続きについては同じクラスにて定義済みです。
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void insertAllRepositories(RoomRepositoryEntity... repositories);
@Delete
public abstract void deleteAllRepositories(RoomRepositoryEntity... repositories);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void insertAllAccounts(RoomAccountEntity... accounts);
また上記の他にオブジェクトとして表現するための Entity クラスと検索結果などを Entity に変換するための Mapper クラスが定義されています。最後に定義されている試験の確認をしておきます。 local
配下には以下の試験があるようです。
- room/dao/RoomAccountDaoTest
- room/dao/RoomRepositoryDaoTest
- room/RoomTest
- GitHubRoomDataSourceTest
ここがおそらく最下層の試験となっているはずです。順に確認していきます。RoomRepositoryDaoTest
から
- DB に保存された全てのリポジトリデータの取得する
getAll
を検証 - ユーザのリポジトリデータを取得する
findByAccountId
を検証 - ユーザ ID のユーザのリポジトリデータを取得する
findByAccount
を検証 - 指定したリポジトリデータをすべて挿入する
insertAllRepositories
を検証 - 指定したリポジトリデータをすべて削除する
deleteAllRepositories
を検証 - 指定したユーザ情報をすべて挿入する
insertAllAccounts
を検証 - 指定したリポジトリとユーザをすべて挿入する
insertRepositoriesAndAccounts
を検証 - 指定したアカウントが持つリポジトリ情報をすべて消して後から追加する
deleteAndInsertRepositoriesAndAccounts
を検証
次は RoomAccountDaoTest
です。
- DB に保存された全てのユーザデータの取得する
getAll
を検証 - ユーザ ID のユーザのユーザデータを取得する
findByAccount
を検証 - 指定したユーザデータをすべて挿入する
insertAll
を検証 - 指定したユーザデータをすべて削除する
deleteAll
を検証
次は RoomTest
です。
- ユーザデータを格納する
accountDao
の取得およびアクセサの動作確認を行うaccount_dao_test
- リポジトリデータを格納する
repositoryDao
の取得およびアクセサの動作確認を行うrepository_dao_test
- ユーザデータとリポジトリデータの連携を確認する
repo_with_account_test
RoomRepositoryDao#insertRepositoriesAndAccounts
の動作確認を行うdeleteAndInsert
最後に GitHubRoomDataSourceTest
です。
GitHubRoomDataSource#getUserRepositories(String)
を呼ぶと内部でRoomRepositoryDao#findByAccount(String)
が呼ばれることを検証し、RepositoryEntityMapper#convertList(List)
が内部で呼ばれることも検証GitHubRoomDataSource#insertRepositoriesAndAccounts(List, List)
を呼ぶと内部でRepositoryDao#insertRepositoriesAndAccounts(List, List)
が呼ばれることを検証GitHubRoomDataSource#deleteAndInsertRepositoriesAndAccounts(String, List, List)
を呼ぶと内部でRoomRepositoryDao#deleteAndInsertRepositoriesAndAccounts(String, List, List)
が呼ばれることを検証
次は datasource/remote
側の実装の確認を行っていきます。こちらも GitHubRemoteDataSource
から確認を。こちらも interface な定義になっています。
public interface GitHubRemoteDataSource {
@NonNull
List<RepositoryEntity> getUserRepositories(@NonNull String account) throws IOException, ApiException;
}
これを実装しているのが GitHubRestDataSource
で定義の先頭部分から確認すすめます。
public class GitHubRestDataSource extends BaseRestDataSource<List<RepositoryEntity>> implements GitHubRemoteDataSource {
@NonNull
private final GitHubService gitHubService;
public GitHubRestDataSource(@NonNull Gson gson, @NonNull GitHubService gitHubService) {
super(gson);
this.gitHubService = gitHubService;
}
GitHubService
型の属性は @NonNull でコンストラクタで渡されています。後は実装を強制されているメソッドと sort key と並び順な enum になっています。メソッド定義は以下で
@NonNull
@Override
public List<RepositoryEntity> getUserRepositories(@NonNull String account) throws IOException, ApiException {
Response<List<RepositoryEntity>> response = gitHubService.getUserRepositories(
account,
Sort.UPDATED.value,
Direction.DESC.value
).execute();
return stripResult(response);
}
って、このあたりで派手に道に迷っていたのですが、そもそも GitHubInfraRepository
の Factory method にて GitHubRemoteDataSource
が生成されているのでした。
GitHubRemoteDataSource remote = new GitHubRestDataSource(
GsonProvider.INSTANCE.get(),
RetrofitProvider.INSTANCE.get().create(GitHubService.class)
);
ええと、GitHubService
型な属性には datasource/remote/service
配下に定義されている RetrofitProvider
の instance 属性 (Retrofit 型) の create が戻すオブジェクトが、という事なのか。
そしてそのオブジェクトであるところの GitHubService
は datasource/remote/service
配下にて定義されています。interface ですね。以下に定義を引用します。
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<RepositoryEntity>> getUserRepositories(
@Path("user") String user,
@Query("sort") String sort,
@Query("direction") String direction
);
}
このあたりはこんなもんだ、って理解で良いのかどうか。別途で再確認した方が良さげですね。このまますすめます。
あと、以下な記述を再度引用するんですが
Response<List<RepositoryEntity>> response = gitHubService.getUserRepositories(
account,
Sort.UPDATED.value,
Direction.DESC.value
).execute();
execute しないと非同期通信しない、という理解で良いのかどうか (別途確認
要確認
RepositoryMapper
RepositoryEntityMapper
- Retrfit のあたり諸々