機能リリースフラグが付いたコードをテストする

機能リリースフラグの導入に伴い、遵守すべき新しいテストポリシーが設けられました。

  • テストでは、フラグが有効な場合と無効な場合の両方の動作をカバーする必要があります。
  • テスト中にフラグの値を設定するには、公式メカニズムを使用する必要があります。
  • xTS テストでは、テスト内のフラグ値をオーバーライドしないでください。

次のセクションでは、これらのポリシーを遵守するために使用すべき公式メカニズムについて説明します。

フラグが付けられたコードをテストする

テストシナリオ 使用するメカニズム
フラグ値が頻繁に変更される場合のローカルテスト Android Debug Bridge(実行時にフラグの値を変更するで説明)
フラグ値が頻繁に変更されない場合のローカルテスト フラグ値ファイル(機能リリースフラグの値を設定するで説明)
フラグ値が変更されるエンドツーエンド テスト FeatureFlagTargetPreparerエンドツーエンド テストを作成するで説明)
フラグ値が変更される単体テスト @EnableFlags@DisableFlags を使用した SetFlagsRule単体テストを作成する(Java および Kotlin)または単体テストを作成する(C および C++)で説明)
フラグ値を変更できないエンドツーエンド テストまたは単体テスト CheckFlagsRuleフラグ値が変更されないエンドツーエンド テストまたは単体テストを作成するで説明)

エンドツーエンド テストを作成する

AOSP には、デバイス上でのエンドツーエンド テストを可能にする FeatureFlagTargetPreparer というクラスが用意されています。このクラスは、フラグ値のオーバーライドを入力として受け取り、テスト実行前にデバイス設定でそれらのフラグを設定し、実行後にフラグを復元します。

FeatureFlagTargetPreparer クラスの機能は、テスト モジュール レベルとテスト構成レベルで適用できます。

テスト モジュール設定で FeatureFlagTargetPreparer を適用する

テスト モジュール設定で FeatureFlagTargetPreparer を適用するには、FeatureFlagTargetPreparer とフラグ値のオーバーライドを AndroidTest.xml テスト モジュール構成ファイルに含めます。

  <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
        <option name="flag-value"
            value="permissions/com.android.permission.flags.device_aware_permission_grant=true"/>
        <option name="flag-value"
            value="virtual_devices/android.companion.virtual.flags.stream_permissions=true"/>
    </target_preparer>

ここで、

  • target.preparer class は常に com.android.tradefed.targetprep.FeatureFlagTargetPreparer に設定されます。
  • option がフラグのオーバーライドで、name は常に flag-value に設定され、valuenamespace/aconfigPackage.flagName=true|false に設定されます。

フラグの状態に基づいてパラメータ化されたテスト モジュールを作成する

フラグの状態に基づいてパラメータ化されたテスト モジュールを作成するには:

  1. FeatureFlagTargetPreparerAndroidTest.xml テスト モジュール構成ファイルに含めます。

    <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" >
    
  2. Android.bp ビルドファイルの test_module_config セクションで、フラグ値のオプションを指定します。

    android_test {
        name: "MyTest"
        ...
    }
    
    test_module_config {
        name: "MyTestWithMyFlagEnabled",
        base: "MyTest",
        ...
        options: [
            {name: "flag-value", value: "telephony/com.android.internal.telephony.flags.oem_enabled_satellite_flag=true"},
        ],
    }
    
    test_module_config {
        name: "MyTestWithMyFlagDisabled",
        base: "MyTest",
        ...
        options: [
            {name: "flag-value", value: "telephony/com.android.internal.telephony.flags.carrier_enabled_satellite_flag=true"},
        ],
    }
    

    options フィールドには、フラグのオーバーライドが含まれます。name は常に flag-value に設定され、valuenamespace/aconfigPackage.flagName=true|false に設定されます。

単体テストを作成する(Java および Kotlin)

このセクションでは、Java および Kotlin のテストにおいて、クラスレベルとメソッドレベル(テストごと)で aconfig フラグ値をオーバーライドする方法について説明します。

フラグが多数ある大規模なコードベースで自動単体テストを作成するには、次の手順を行います。

  1. @EnableFlags アノテーションと @DisableFlags アノテーションを使用して SetFlagsRule クラスを使用すると、すべてのコードブランチをテストできます。
  2. SetFlagsRule.ClassRule メソッドを使用して、一般的なテストバグを回避します。
  3. FlagsParameterization を使用して、さまざまなフラグ構成でクラスをテストします。

すべてのコードブランチをテストする

静的クラスを使用してフラグにアクセスするプロジェクトの場合、フラグ値をオーバーライドするための SetFlagsRule ヘルパークラスが用意されています。次のコード スニペットは、SetFlagsRule を使用して複数のフラグを一度に有効にする方法を示しています。

  import android.platform.test.annotations.EnableFlags;
  import android.platform.test.flag.junit.SetFlagsRule;
  import com.example.android.aconfig.demo.flags.Flags;
  ...
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Test
    @EnableFlags({Flags.FLAG_FLAG_FOO, Flags.FLAG_FLAG_BAR})
    public void test_flag_foo_and_flag_bar_turned_on() {
    ...
    }

ここで、

  • @Rule は、SetFlagsRule クラスでのフラグと JUnit の依存関係を追加するために使用されるアノテーションです。
  • SetFlagsRule は、フラグ値をオーバーライドするために用意されたヘルパークラスです。SetFlagsRule でデフォルト値がどのように決まるかについては、デバイスのデフォルト値をご覧ください。
  • @EnableFlags は、任意の数のフラグ名を受け入れるアノテーションです。フラグを無効にする場合は、@DisableFlags を使用します。これらのアノテーションは、メソッドまたはクラスに適用できます。

テストプロセス全体にフラグ値を設定するために、まず SetFlagsRule を設定します。これは、テスト内の @Before アノテーションが付いたセットアップ メソッドの前に実行されます。SetFlagsRule が終了すると、フラグ値は元の状態に戻ります。これは、@After アノテーションが付いたセットアップ メソッドの後に発生します。

フラグが正しく設定されるようにする

前述のように、SetFlagsRule は JUnit の @Rule アノテーションで使用されます。このため SetFlagsRule では、テストクラスのコンストラクタや、@BeforeClass または @AfterClass アノテーション付きのメソッドでフラグが正しく設定されることを保証できません。

テスト フィクスチャが正しいクラス値で作成されるようにするには、SetFlagsRule.ClassRule メソッドを使用して、@Before アノテーションが付いたセットアップ メソッドが実行されるまでフィクスチャが作成されないようにします。

  import android.platform.test.annotations.EnableFlags;
  import android.platform.test.flag.junit.SetFlagsRule;
  import com.example.android.aconfig.demo.flags.Flags;

  class ExampleTest {
    @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
    @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();

    private DemoClass underTest = new DemoClass();

    @Test
    @EnableFlags(Flags.FLAG_FLAG_FOO)
    public void test_flag_foo_turned_on() {
      ...
    }
  }

SetFlagsRule.ClassRule クラスルールを追加すると、DemoClass のコンストラクタによって FLAG_FLAG_FOO が読み取られたときに、test_flag_foo_turned_on が実行されずに失敗します。

クラス全体でフラグを有効にする必要がある場合は、@EnableFlags アノテーションをクラスレベル(クラス宣言の前)に移動します。アノテーションをクラスレベルに移動すると、SetFlagsRule.ClassRule は、テストクラスのコンストラクタや、@BeforeClass または @AfterClass アノテーション付きのメソッドでもフラグを正しく設定できるようになります。

複数のフラグ構成でテストを実行する

フラグ値はテストごとに設定できるため、パラメータ化を使用して複数のフラグ構成でテストを実行することもできます。

...
import com.example.android.aconfig.demo.flags.Flags;
...

@RunWith(ParameterizedAndroidJunit4::class)
class FooBarTest {
    @Parameters(name = "{0}")
    public static List<FlagsParameterization> getParams() {
        return FlagsParameterization.allCombinationsOf(Flags.FLAG_FOO, Flags.FLAG_BAR);
    }

    @Rule
    public SetFlagsRule mSetFlagsRule;

    public FooBarTest(FlagsParameterization flags) {
        mSetFlagsRule = new SetFlagsRule(flags);
    }

    @Test public void fooLogic() {...}

    @DisableFlags(Flags.FLAG_BAR)
    @Test public void legacyBarLogic() {...}

    @EnableFlags(Flags.FLAG_BAR)
    @Test public void newBarLogic() {...}
}

SetFlagsRule を設定し、パラメータ化をしなかった場合、このクラスは 3 件のテスト(fooLogiclegacyBarLogicnewBarLogic)を実行します。fooLogic メソッドは、デバイスで設定された FLAG_FOO および FLAG_BAR の値で実行されます。

パラメータ化が追加されると、FlagsParameterization.allCombinationsOf メソッドは、FLAG_FOO フラグと FLAG_BAR フラグの可能なすべての組み合わせを作成します。

  • FLAG_FOOtrueFLAG_BARtrue
  • FLAG_FOOtrueFLAG_BARfalse
  • FLAG_FOOfalseFLAG_BARtrue
  • FLAG_FOO は false、FLAG_BARfalse

@DisableFlags アノテーションと @EnableFlags アノテーションは、フラグ値を直接変更するのではなく、パラメータ条件に基づいてフラグ値を変更します。たとえば、legacyBarLogicFLAG_BAR が無効になっている場合にのみ実行されます。これは、フラグの 4 つの組み合わせのうち 2 つで発生します。他の 2 つの組み合わせでは、legacyBarLogic はスキップされます。

フラグをパラメータ化するには、次の 2 つのメソッドがあります。

  • FlagsParameterization.allCombinationsOf(String...) は、各テストを 2^n 回実行します。たとえば、フラグが 1 つだと 2 回のテストが実行され、フラグが 4 つだと 16 回のテストが実行されます。

  • FlagsParameterization.progressionOf(String...) は、各テストを n+1 回実行します。たとえば、フラグが 1 つだと 2 回のテストが実行され、フラグが 4 つだと 5 回のテストが実行されます。

単体テストを作成する(C および C++)

AOSP には、GoogleTest フレームワークで記述された C および C++ テスト用のフラグ値マクロが含まれています。

  1. テストソースに、マクロ定義と aconfig で生成されたライブラリを含めます。

    #include <flag_macros.h>
    #include "android_cts_flags.h"
    
  2. テストソースで、テストケースに TEST マクロと TESTF マクロを使用する代わりに、TEST_WITH_FLAGSTEST_F_WITH_FLAGS を使用します。

    #define TEST_NS android::cts::flags::tests
    
    ...
    
    TEST_F_WITH_FLAGS(
      TestFWithFlagsTest,
      requies_disabled_flag_enabled_skip,
      REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(TEST_NS, readwrite_enabled_flag))
    ) {
      TestFail();
    }
    
    ...
    
    TEST_F_WITH_FLAGS(
      TestFWithFlagsTest,
      multi_flags_for_same_state_skip,
      REQUIRES_FLAGS_ENABLED(
          ACONFIG_FLAG(TEST_NS, readwrite_enabled_flag),
          LEGACY_FLAG(aconfig_flags.cts, TEST_NS, readwrite_disabled_flag)
      )
    ) {
      TestFail();
    }
    
    ...
    
    TEST_WITH_FLAGS(
      TestWithFlagsTest,
      requies_disabled_flag_enabled_skip,
      REQUIRES_FLAGS_DISABLED(
          LEGACY_FLAG(aconfig_flags.cts, TEST_NS, readwrite_enabled_flag))
    ) {
      FAIL();
    }
    
    ...
    
    TEST_WITH_FLAGS(
      TestWithFlagsTest,
      requies_enabled_flag_enabled_executed,
      REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(TEST_NS, readwrite_enabled_flag))
    ) {
      TestWithFlagsTestHelper::executed_tests.insert(
          "requies_enabled_flag_enabled_executed");
    }
    

    ここで、

    • TEST マクロと TEST_F マクロの代わりに TEST_WITH_FLAGS マクロと TEST_F_WITH_FLAGS マクロが使われます。
    • REQUIRES_FLAGS_ENABLED は、有効化条件を満たす必要がある一連の機能リリースフラグを定義します。このフラグは、ACONFIG_FLAG マクロまたは LEGACY_FLAG マクロで記述できます。
    • REQUIRES_FLAGS_DISABLED は、無効化条件を満たす必要がある一連の機能フラグを定義します。このフラグは、ACONFIG_FLAG マクロまたは LEGACY_FLAG マクロで記述できます。
    • ACONFIG_FLAG (TEST_NS, readwrite_enabled_flag) は、aconfig ファイルで定義されたフラグに使用されるマクロです。このマクロは、名前空間(TEST_NS)とフラグ名(readwrite_enabled_flag)を受け入れます。
    • LEGACY_FLAG(aconfig_flags.cts, TEST_NS, readwrite_disabled_flag) は、デバイス設定でデフォルトで設定されるフラグに使用されるマクロです。
  3. Android.bp ビルドファイルに、aconfig で生成されたライブラリと、関連するマクロ ライブラリをテスト依存関係として追加します。

    cc_test {
      name: "FlagMacrosTests",
      srcs: ["src/FlagMacrosTests.cpp"],
      static_libs: [
          "libgtest",
          "libflagtest",
          "my_aconfig_lib",
      ],
      shared_libs: [
          "libbase",
          "server_configurable_flags",
      ],
      test_suites: ["general-tests"],
      ...
    }
    
  4. 次のコマンドを使用して、ローカルでテストを実行します。

    atest FlagMacrosTests
    

    フラグ my_namespace.android.myflag.tests.my_flag が無効になっている場合、テスト結果は次のようになります。

    [1/2] MyTest#test1: IGNORED (0ms)
    [2/2] MyTestF#test2: PASSED (0ms)
    

    フラグ my_namespace.android.myflag.tests.my_flag が有効になっている場合、テスト結果は次のようになります。

    [1/2] MyTest#test1: PASSED (0ms)
    [2/2] MyTestF#test2: IGNORED (0ms)
    

フラグ値が変更されないエンドツーエンド テストまたは単体テストを作成する

フラグをオーバーライドできず、現在のフラグ状態に基づいている場合のみテストをフィルタリングできるテストケースの場合は、RequiresFlagsEnabled アノテーションと RequiresFlagsDisabled アノテーションでルール CheckFlagsRule を使用します。

次の手順では、フラグ値をオーバーライドできないエンドツーエンド テストまたは単体テストを作成して実行する方法について説明します。

  1. テストコードで、CheckFlagsRule を使用してテスト フィルタリングを適用します。また、Java アノテーション RequiresFlagsEnabledRequiredFlagsDisabled を使用して、テストのフラグ要件を指定します。

    デバイス側のテストでは、DeviceFlagsValueProvider クラスを使用します。

    @RunWith(JUnit4.class)
    public final class FlagAnnotationTest {
      @Rule
      public final CheckFlagsRule mCheckFlagsRule =
              DeviceFlagsValueProvider.createCheckFlagsRule();
    
      @Test
      @RequiresFlagsEnabled(Flags.FLAG_FLAG_NAME_1)
      public void test1() {}
    
      @Test
      @RequiresFlagsDisabled(Flags.FLAG_FLAG_NAME_1)
      public void test2() {}
    }
    

    ホスト側のテストでは、HostFlagsValueProvider クラスを使用します。

    @RunWith(DeviceJUnit4ClassRunner.class)
    public final class FlagAnnotationTest extends BaseHostJUnit4Test {
      @Rule
      public final CheckFlagsRule mCheckFlagsRule =
              HostFlagsValueProvider.createCheckFlagsRule(this::getDevice);
    
      @Test
      @RequiresFlagsEnabled(Flags.FLAG_FLAG_NAME_1)
      public void test1() {}
    
      @Test
      @RequiresFlagsDisabled(Flags.FLAG_FLAG_NAME_1)
      public void test2() {}
    }
    
  2. テストのビルドファイルの static_libs セクションに、jflag-unit と aconfig で生成されたライブラリを追加します。

    android_test {
        name: "FlagAnnotationTests",
        srcs: ["*.java"],
        static_libs: [
            "androidx.test.rules",
            "my_aconfig_lib",
            "flag-junit",
            "platform-test-annotations",
        ],
        test_suites: ["general-tests"],
    }
    
  3. テストをローカルで実行するには、次のコマンドを使用します。

    atest FlagAnnotationTests
    

    フラグ Flags.FLAG_FLAG_NAME_1 が無効になっている場合、テスト結果は次のようになります。

    [1/2] com.cts.flags.FlagAnnotationTest#test1: ASSUMPTION_FAILED (10ms)
    [2/2] com.cts.flags.FlagAnnotationTest#test2: PASSED (2ms)
    

    それ以外の場合、テスト結果は次のようになります。

    [1/2] com.cts.flags.FlagAnnotationTest#test1: PASSED (2ms)
    [2/2] com.cts.flags.FlagAnnotationTest#test2: ASSUMPTION_FAILED (10ms)
    

デバイスのデフォルト値

初期化された SetFlagsRule は、デバイスのフラグ値を使用します。adb などでデバイスのフラグ値がオーバーライドされていない場合、デフォルト値はビルドのリリース構成と同じです。デバイスの値がオーバーライドされている場合、SetFlagsRule はオーバーライドされた値をデフォルトとして使用します。

同じテストを異なるリリース構成で実行すると、SetFlagsRule で明示的に設定されていないフラグの値が異なる場合があります。

各テストの後に、SetFlagsRuleFlagsFeatureFlags インスタンスを元の FeatureFlagsImpl に復元します。これにより、他のテストメソッドやクラスに副作用が及ぶことがなくなります。