はじめに
JavaのAWS SDKを使って、cognitoの情報を取得するコードを書いて、テストで動作確認しようとしたら詰まった。依存関係が深くなってくると、作成したインスタンス内の@Inject
が適応されないらしい。@QuarkusTest
を使えば、ランタイム環境まで模倣されるので複雑な依存関係もちゃんと動くことが分かった。
上手く行ったコード
プロジェクト環境
Java: 17.0.9
Quarkus: 3.2.7.Final
Gradle: 7.6.3
cognitoユーザープールに登録されているユーザーの一覧を取得してみる。
// /config/AWSClientConfig.java
import io.quarkus.runtime.annotations.StaticInitSafe;
import io.smallrye.config.ConfigMapping;
@StaticInitSafe
@ConfigMapping(prefix = "aws.credentials")
public interface AWSClientConfig {
String accessKey();
String secretKey();
}
// /config/CognitoClientConfig.java
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
import software.amazon.awssdk.regions.Region;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class CognitoClientConfig {
@Inject
public AWSClientConfig config;
public CognitoIdentityProviderClient getCognitoClient(){
return CognitoIdentityProviderClient.builder()
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(config.accessKey(), config.secretKey())))
.build();
}
}
複数コードで呼び出すからconfigフォルダに設定類を記載。AWSClientConfig
とCognitoClientConfig
で分けるのは必須ではないが、他サービスも使うなら
AWSClientConfig
を継承する形が分かりやすい。
// /main/UserService.java
import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
import software.amazon.awssdk.services.cognitoidentityprovider.model.*;
import your.project.config.AWSClientConfig;
import your.project.config.CognitoClientConfig;
import your.project.data.user.output.FindAllUserOutput;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class UserService {
@Inject
CognitoClientConfig cognitoClientConfig; // コンフィグ情報をインジェクト
public FindAllUserOutput FindAll() {
FindAllUserOutput output = new FindAllUserOutput();
CognitoIdentityProviderClient cognitoClient = cognitoClientConfig.getCognitoClient();
try {
ListUsersRequest request = ListUsersRequest.builder()
.userPoolId(YOUR_USER_POOL_ID)
.build();
ListUsersResponse response = cognitoClient.listUsers(request);
response.users().forEach(user -> {
String userName = user.username();
String emailAddress = user.attributes().stream() //他属性があれば追加
.filter(attribute -> attribute.name().equals("email"))
.findFirst()
.map(AttributeType::value)
.orElse("メールアドレスが見つかりません");
System.out.println("User name: " + userName + ", Email address: " + emailAddress);
});
} catch (CognitoIdentityProviderException e){
System.err.println(e.awsErrorDetails().errorMessage());
} catch (Exception e) {
System.err.println("エラーが発生しました: " + e.getMessage());
}
return output;
}
}
// /test/UserServiceTest.java
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import io.quarkus.test.junit.QuarkusTest;
import your.project.data.user.output.FindAllUserOutput;
@QuarkusTest // ここでランタイムを模倣
public class UserServiceTest {
@Inject
private UserService userService;
@Test
void testListAgent () {
FindAllUserOutput output = userService.FindAll();
}
}
ハマりポイント
最初はそのままテストコードでService
クラスを@Inject
して、new
でインスタンスを新規作成を試みたが、その後のservice内のメソッドが呼び出せなかった。原因は依存しているコンフィグがnullであったこと。
java.lang.NullPointerException: Cannot invoke "your.project.config.AWSClientConfig.accessKey()" because "this.config" is null
振り返ってみると、今回のコードは以下のような依存関係にあった。
UserService.java
↓依存
CognitoClientConfig.java
↓依存
AWSClientConfig.java
どうやら、Java標準のnew
を使ってインスタンスを作成すると、そのインスタンスはQuarkusのランタイムコンテキストの外で作成されるため、依存性注入が行われないらしい。つまり、newキーワードで作成されたオブジェクトは、その内部で@Inject
や他のQuarkusの機能を使用することができないことが分かった。
そこで@QuarkusTest
を使用すると、Quarkusアプリケーションのコンポーネントやサービスをテストする際に、QuarkusのDI機能がサポートされるようになる。
副作用というほどでもないが、@QuarkusTest
を使用するとQuarkusのランタイム環境を模倣されるため、DBの接続や他のクレデンシャルを必要とするコードがあると、設定が少し面倒になる。