// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1
アーキテクチャテストのモチベーション
集約を構成するオブジェクトは、データベース等の永続化層から、個別に参照や更新するのではなく、集約ルートを起点として集約(オブジェクトのまとまり)としての整合性を保ちながら、参照や更新したい。
アーキテクチャテストの実装
テスト対象の集約とクラスのサンプルは後述。
package com.example;
import com.example.domain.order.DenpyoAggregateDao;
import com.example.domain.order.DenpyoDao;
import com.example.domain.order.MeisaiDao;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.theClass;
class ArchitectureTest {
// 検査対象のクラス
private static final JavaClasses CLASSES =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.example");
@ParameterizedTest
@ValueSource(classes = {
// 集約を構成する各エンティティに対応するDAO
DenpyoDao.class,
MeisaiDao.class
})
void 集約を構成する各エンティティに対応するDAOは集約操作専用のDAOによってのみ操作される(
final Class<?> daoClass
) {
// 集約操作専用のDAO
Class<DenpyoAggregateDao> aggregateDaoClass = DenpyoAggregateDao.class;
theClass(daoClass)
.should()
.onlyBeAccessed()
.byClassesThat()
.haveFullyQualifiedName(aggregateDaoClass.getName())
.check(CLASSES);
}
}
(参考)テスト対象の集約のサンプル
以下のような集約があるとする。
エンティティの識別子を表す値オブジェクト
public final class Identity<ENTITY> {
//...
}
伝票エンティティと Dao
@Entity
public class Denpyo {
@Id
Identity<Denpyo> id;
@Transient
List<Meisai> meisaiList;
}
public interface DenpyoDao {
Optional<Denpyo> findById(Identity<Denpyo> id);
int insert(Denpyo denpyo);
int update(Denpyo denpyo);
}
明細エンティティと Dao
@Entity
public class Meisai {
@Id
Identity<Meisai> id;
Identity<Denpyo> denpyoId;
}
public interface MeisaiDao {
List<Meisai> findByDenpyoId(Identity<Denpyo> denpyoId);
int[] insert(List<Meisai> meisaiList);
int[] update(List<Meisai> meisaiList);
}
伝票・明細を集約として操作する Aggregate Dao
public class DenpyoAggregateDao {
private final DenpyoDao denpyoDao;
private final MeisaiDao meisaiDao;
public DenpyoAggregateDao(final DenpyoDao denpyoDao, final MeisaiDao meisaiDao) {
this.denpyoDao = denpyoDao;
this.meisaiDao = meisaiDao;
}
// 集約の取得
Optional<Denpyo> findById(final Identity<Denpyo> id) {
return denpyoDao.findById(id).map(denpyo -> {
denpyo.meisaiList = meisaiDao.findByDenpyoId(denpyo.id);
return denpyo;
});
}
// 集約の登録
@Transactional
void insert(final Denpyo denpyo) {
assert denpyo.meisaiList != null;
denpyoDao.insert(denpyo);
// `伝票ID`がデータベース等により自動採番される場合はその値を`明細.伝票ID`に反映する
// denpyo.meisaiList.forEach(meisai -> meisai.denpyoId = denpyo.id);
meisaiDao.insert(denpyo.meisaiList);
}
// 集約の更新
@Transactional
void update(final Denpyo denpyo) {
assert denpyo.meisaiList != null;
denpyoDao.update(denpyo);
meisaiDao.update(denpyo.meisaiList);
}
}