はじめに
これどうするんだっけ?となりドキュメントを見に行くことが(個人的に)多い OCI Functions 開発におけるチートシートです。内容は順次拡張します。
Fn Test Harness
Request Header/Body をエミュレートしたい
詳細は、FDK Java - Testing data binding and configuration から。
単純な文字列を受け取る
以下のような Function で input を受け取った場合のテストをしたいとします。
public class HelloFunction {
public String handleRequest(String input) {
String name = (input == null || input.isEmpty()) ? "world" : input;
System.out.println("Inside Java Hello World function");
return "Hello, " + name + "!";
}
}
→ FnEventBuilderJUnit4#withBody, FnEventBuilderJUnit4#withHeaderを使う
Request Body に関わらず、Event をエミュレートするためのインターフェースが com.fnproject.fn.testing.FnEventBuilder から提供されています。
FnEventBuilderJUnit4 はこのインターフェースの実装クラスです。
import com.fnproject.fn.testing.*;
import org.junit.*;
import static org.junit.Assert.*;
public class HelloFunctionTest {
@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();
@Test
public void shouldReturnGreetingWithBody() {
testing.givenEvent().withBody("John").enqueue();
testing.thenRun(HelloFunction.class, "handleRequest");
FnResult result = testing.getOnlyResult();
assertEquals("Hello, John!", result.getBodyAsString());
}
}
JSON を受け取る
以下のような JSON を受け取る Function のテストをしたいとします。
public class HelloJsonFunction {
public String handleRequest(Input input) {
System.out.println("Inside Java Hello World function");
return "Hello, " + input.name() + "!";
}
}
public record Input(String name) {
}
FDK(Function Development Kits)には、Jackson への依存が含まれているので、以下のように実装すると良いでしょう。
import static org.junit.Assert.assertEquals;
import org.junit.Rule;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fnproject.fn.testing.FnResult;
import com.fnproject.fn.testing.FnTestingRule;
public class HelloJsonFunctionTest {
@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();
@Test
public void shouldReturnGreetingWithBody() {
var mapper = new ObjectMapper();
try {
testing
.givenEvent()
.withHeader("content-type", "application/json") // Request Header
.withBody(mapper.writeValueAsString(new Input("John"))) // Request Body
.enqueue();
testing.thenRun(HelloJsonFunction.class, "handleRequest");
} catch (JsonProcessingException e) {
// do something.
}
FnResult result = testing.getOnlyResult();
assertEquals("Hello, John!", result.getBodyAsString());
}
}
Config を使いたい
OCID(OCI のリソースに付与された ID)やデータベースの接続先(を参照する Vault の OCID)などは、Config に持っておくケースがあると思います。
schema_version: 20180708
name: test-function
version: 0.0.1
runtime: java
build_image: fnproject/fn-java-fdk-build:jdk17-1.0.177
run_image: fnproject/fn-java-fdk:jre17-1.0.177
cmd: com.example.fn.HelloFunction::handleRequest
config:
greet: Hi
@FnConfiguration を用いるパターン
import com.fnproject.fn.api.FnConfiguration;
import com.fnproject.fn.api.RuntimeContext;
public class HelloConfigFunction {
private String greet;
@FnConfiguration
public void config(RuntimeContext ctx) {
greet = ctx.getConfigurationByKey("greet").orElse("Hello");
}
public String handleRequest(String input) {
String name = (input == null || input.isEmpty()) ? "world" : input;
System.out.println("Inside Java Hello World function");
return greet + name + "!";
}
}
final + constructor でも実装出来ますが、Function の実行毎に呼びだされるので、Runtime 毎に 1 回(Warm Start として使いまわされる単位で 1 回)だけ実行したい場合は、素直に @FnConfiguration を使うと良いでしょう。(例えば、データベースへのコネクション定義なんかは、@FnConfiguration を付けたメソッド内で行うのが良いです。)
import com.fnproject.fn.api.RuntimeContext;
public class HelloConstructorConfigFunction {
private final String greet;
public HelloConstructorConfigFunction(RuntimeContext ctx) {
greet = ctx.getConfigurationByKey("GREET").orElse("Hello");
}
public String handleRequest(String input) {
String name = (input == null || input.isEmpty()) ? "world" : input;
System.out.println("Inside Java Hello World function");
return greet + ", " + name + "!";
}
}
テストは以下のように書くことができます。
import static org.junit.Assert.assertEquals;
import org.junit.Rule;
import org.junit.Test;
import com.fnproject.fn.testing.FnResult;
import com.fnproject.fn.testing.FnTestingRule;
public class HelloConfigFunctionTest {
@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();
@Test
public void shouldReturnDefaultGreeting() {
testing.givenEvent().enqueue();
testing.thenRun(HelloConfigFunction.class, "handleRequest");
FnResult result = testing.getOnlyResult();
assertEquals("Hello, world!", result.getBodyAsString());
}
@Test
public void shouldReturnGreetingWithConfig() {
testing.setConfig("greet", "Hi").givenEvent().withBody("John").enqueue();
testing.thenRun(HelloConfigFunction.class, "handleRequest");
FnResult result = testing.getOnlyResult();
assertEquals("Hi, John!", result.getBodyAsString());
}
@Test
public void shouldReturnGreetingWithConstructorConfig() {
testing.setConfig("greet", "Hi").givenEvent().withBody("John").enqueue();
testing.thenRun(HelloConstructorConfigFunction.class, "handleRequest");
FnResult result = testing.getOnlyResult();
assertEquals("Hi, John!", result.getBodyAsString());
}
}
FnTestingRule#setConfig や Config ファイルから渡した Config 名(ここでいう、"greet")は大文字に置換され、- が含まれている場合は _ に置換されることに注意が必要です。そのため、渡された Configを扱う箇所は、greet = ctx.getConfigurationByKey("GREET").orElse("Hello") と実装しています。
同じイベントを複数回送信してテストをしたい
→ FnEventBuilder#enqueue(int n) を使うと、イベントのコピーを指定回数分(n)送ることができる
import com.fnproject.fn.testing.*;
import org.junit.*;
import static org.junit.Assert.*;
import java.util.List;
public class HelloFunctionTest {
@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();
@Test
public void shouldReturnSameGreeting() {
testing.givenEvent().enqueue(10);
testing.thenRun(HelloFunction.class, "handleRequest");
List<FnResult> results = testing.getResults();
results.forEach(result -> {
assertEquals("Hello, world!", result.getBodyAsString());
});
}
}
結果を受け取る際は、FnTestingRule#getResults を用いると、実行結果が List<FnResult> で受け取れる。
参考