Back to Repositories

Testing Generic Type Token Implementation in google/gson

This test suite evaluates the TypeToken implementation in Google’s Gson library, focusing on type safety, generic type handling, and reflection capabilities. The tests verify correct behavior of type assignments, parameterized types, and various edge cases in Java’s generic type system.

Test Coverage Overview

The test suite provides comprehensive coverage of TypeToken functionality, including type assignment compatibility, generic type parameters, and wildcard types. Key test areas include:
  • Raw type assignments and compatibility checks
  • Parameterized type handling with generic bounds
  • Array type creation and validation
  • Nested generic type support
  • Type variable capture and validation

Implementation Analysis

The testing approach uses JUnit framework with a combination of positive and negative test cases. The implementation focuses on type safety verification using reflection APIs and custom type token creation patterns. Notable patterns include:
  • Type assignment verification using isAssignableFrom
  • Factory method testing for parameterized types
  • Boundary testing for generic type constraints
  • Error handling for invalid type configurations

Technical Details

Testing infrastructure includes:
  • JUnit 4 test framework
  • Google Truth assertion library
  • Java Reflection API utilities
  • Custom test classes with generic bounds
  • System property controls for type variable capture

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Thorough validation of edge cases and error conditions
  • Clear test method organization and naming
  • Comprehensive assertion messages
  • Proper setup of test hierarchies
  • Effective use of test annotations and suppressions

google/gson

gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java

            
/*
 * Copyright (C) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gson.reflect;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;
import org.junit.Test;

/**
 * Tests for {@link TypeToken}.
 *
 * @author Jesse Wilson
 */
// Suppress because these classes are only needed for this test, but must be top-level classes
// to not have an enclosing type
@SuppressWarnings("MultipleTopLevelClasses")
public final class TypeTokenTest {
  // These fields are accessed using reflection by the tests below
  List<Integer> listOfInteger = null;
  List<Number> listOfNumber = null;
  List<String> listOfString = null;
  List<?> listOfUnknown = null;
  List<Set<String>> listOfSetOfString = null;
  List<Set<?>> listOfSetOfUnknown = null;

  @SuppressWarnings({"deprecation"})
  @Test
  public void testIsAssignableFromRawTypes() {
    assertThat(TypeToken.get(Object.class).isAssignableFrom(String.class)).isTrue();
    assertThat(TypeToken.get(String.class).isAssignableFrom(Object.class)).isFalse();
    assertThat(TypeToken.get(RandomAccess.class).isAssignableFrom(ArrayList.class)).isTrue();
    assertThat(TypeToken.get(ArrayList.class).isAssignableFrom(RandomAccess.class)).isFalse();
  }

  @SuppressWarnings({"deprecation"})
  @Test
  public void testIsAssignableFromWithTypeParameters() throws Exception {
    Type a = getClass().getDeclaredField("listOfInteger").getGenericType();
    Type b = getClass().getDeclaredField("listOfNumber").getGenericType();
    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();

    // listOfInteger = listOfNumber; // doesn't compile; must be false
    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
    // listOfNumber = listOfInteger; // doesn't compile; must be false
    assertThat(TypeToken.get(b).isAssignableFrom(a)).isFalse();
  }

  @SuppressWarnings({"deprecation"})
  @Test
  public void testIsAssignableFromWithBasicWildcards() throws Exception {
    Type a = getClass().getDeclaredField("listOfString").getGenericType();
    Type b = getClass().getDeclaredField("listOfUnknown").getGenericType();
    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();

    // listOfString = listOfUnknown  // doesn't compile; must be false
    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
    listOfUnknown = listOfString; // compiles; must be true
    // The following assertion is too difficult to support reliably, so disabling
    // assertThat(TypeToken.get(b).isAssignableFrom(a)).isTrue();

    WildcardType wildcardType = (WildcardType) ((ParameterizedType) b).getActualTypeArguments()[0];
    TypeToken<?> wildcardTypeToken = TypeToken.get(wildcardType);
    IllegalArgumentException e =
        assertThrows(IllegalArgumentException.class, () -> wildcardTypeToken.isAssignableFrom(b));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Unsupported type, expected one of: java.lang.Class,"
                + " java.lang.reflect.ParameterizedType, java.lang.reflect.GenericArrayType, but"
                + " got: com.google.gson.internal.$Gson$Types$WildcardTypeImpl, for type token: "
                + wildcardTypeToken);
  }

  @SuppressWarnings({"deprecation"})
  @Test
  public void testIsAssignableFromWithNestedWildcards() throws Exception {
    Type a = getClass().getDeclaredField("listOfSetOfString").getGenericType();
    Type b = getClass().getDeclaredField("listOfSetOfUnknown").getGenericType();
    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();

    // listOfSetOfString = listOfSetOfUnknown; // doesn't compile; must be false
    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
    // listOfSetOfUnknown = listOfSetOfString; // doesn't compile; must be false
    assertThat(TypeToken.get(b).isAssignableFrom(a)).isFalse();
  }

  @Test
  public void testArrayFactory() {
    TypeToken<?> expectedStringArray = new TypeToken<String[]>() {};
    assertThat(TypeToken.getArray(String.class)).isEqualTo(expectedStringArray);

    TypeToken<?> expectedListOfStringArray = new TypeToken<List<String>[]>() {};
    Type listOfString = new TypeToken<List<String>>() {}.getType();
    assertThat(TypeToken.getArray(listOfString)).isEqualTo(expectedListOfStringArray);

    TypeToken<?> expectedIntArray = new TypeToken<int[]>() {};
    assertThat(TypeToken.getArray(int.class)).isEqualTo(expectedIntArray);

    assertThrows(NullPointerException.class, () -> TypeToken.getArray(null));
  }

  static class NestedGeneric<T> {}

  @Test
  public void testParameterizedFactory() {
    TypeToken<?> expectedListOfString = new TypeToken<List<String>>() {};
    assertThat(TypeToken.getParameterized(List.class, String.class))
        .isEqualTo(expectedListOfString);

    TypeToken<?> expectedMapOfStringToString = new TypeToken<Map<String, String>>() {};
    assertThat(TypeToken.getParameterized(Map.class, String.class, String.class))
        .isEqualTo(expectedMapOfStringToString);

    TypeToken<?> expectedListOfListOfListOfString = new TypeToken<List<List<List<String>>>>() {};
    Type listOfString = TypeToken.getParameterized(List.class, String.class).getType();
    Type listOfListOfString = TypeToken.getParameterized(List.class, listOfString).getType();
    assertThat(TypeToken.getParameterized(List.class, listOfListOfString))
        .isEqualTo(expectedListOfListOfListOfString);

    TypeToken<?> expectedWithExactArg = new TypeToken<GenericWithBound<Number>>() {};
    assertThat(TypeToken.getParameterized(GenericWithBound.class, Number.class))
        .isEqualTo(expectedWithExactArg);

    TypeToken<?> expectedWithSubclassArg = new TypeToken<GenericWithBound<Integer>>() {};
    assertThat(TypeToken.getParameterized(GenericWithBound.class, Integer.class))
        .isEqualTo(expectedWithSubclassArg);

    TypeToken<?> expectedSatisfyingTwoBounds =
        new TypeToken<GenericWithMultiBound<ClassSatisfyingBounds>>() {};
    assertThat(TypeToken.getParameterized(GenericWithMultiBound.class, ClassSatisfyingBounds.class))
        .isEqualTo(expectedSatisfyingTwoBounds);

    TypeToken<?> nestedTypeToken = TypeToken.getParameterized(NestedGeneric.class, Integer.class);
    ParameterizedType nestedParameterizedType = (ParameterizedType) nestedTypeToken.getType();
    // TODO: This seems to differ from how Java reflection behaves; when using
    // TypeToken<NestedGeneric<Integer>>, then NestedGeneric<Integer> does have an owner type
    assertThat(nestedParameterizedType.getOwnerType()).isNull();
    assertThat(nestedParameterizedType.getRawType()).isEqualTo(NestedGeneric.class);
    assertThat(nestedParameterizedType.getActualTypeArguments())
        .asList()
        .containsExactly(Integer.class);

    class LocalGenericClass<T> {}
    TypeToken<?> expectedLocalType = new TypeToken<LocalGenericClass<Integer>>() {};
    assertThat(TypeToken.getParameterized(LocalGenericClass.class, Integer.class))
        .isEqualTo(expectedLocalType);

    // For legacy reasons, if requesting parameterized type for non-generic class, create a
    // `TypeToken(Class)`
    assertThat(TypeToken.getParameterized(String.class)).isEqualTo(TypeToken.get(String.class));
  }

  @Test
  public void testParameterizedFactory_Invalid() {
    assertThrows(NullPointerException.class, () -> TypeToken.getParameterized(null, new Type[0]));
    assertThrows(
        NullPointerException.class,
        () -> TypeToken.getParameterized(List.class, new Type[] {null}));

    GenericArrayType arrayType = (GenericArrayType) TypeToken.getArray(String.class).getType();
    IllegalArgumentException e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(arrayType, new Type[0]));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo("rawType must be of type Class, but was java.lang.String[]");

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(String.class, Number.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo("java.lang.String requires 0 type arguments, but got 1");

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(List.class, new Type[0]));
    assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0");

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(List.class, String.class, String.class));
    assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 2");

    // Primitive types must not be used as type argument
    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(List.class, int.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument int does not satisfy bounds for type variable E declared by "
                + List.class);

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(GenericWithBound.class, String.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument class java.lang.String does not satisfy bounds"
                + " for type variable T declared by "
                + GenericWithBound.class);

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(GenericWithBound.class, Object.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument class java.lang.Object does not satisfy bounds"
                + " for type variable T declared by "
                + GenericWithBound.class);

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(GenericWithMultiBound.class, Number.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument class java.lang.Number does not satisfy bounds"
                + " for type variable T declared by "
                + GenericWithMultiBound.class);

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(GenericWithMultiBound.class, CharSequence.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument interface java.lang.CharSequence does not satisfy bounds"
                + " for type variable T declared by "
                + GenericWithMultiBound.class);

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(GenericWithMultiBound.class, Object.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Type argument class java.lang.Object does not satisfy bounds"
                + " for type variable T declared by "
                + GenericWithMultiBound.class);

    class Outer {
      @SuppressWarnings("ClassCanBeStatic")
      class NonStaticInner<T> {}
    }

    e =
        assertThrows(
            IllegalArgumentException.class,
            () -> TypeToken.getParameterized(Outer.NonStaticInner.class, Object.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Raw type "
                + Outer.NonStaticInner.class.getName()
                + " is not supported because it requires specifying an owner type");
  }

  private static class CustomTypeToken extends TypeToken<String> {}

  @Test
  public void testTypeTokenNonAnonymousSubclass() {
    TypeToken<?> typeToken = new CustomTypeToken();
    assertThat(typeToken.getRawType()).isEqualTo(String.class);
    assertThat(typeToken.getType()).isEqualTo(String.class);
  }

  /**
   * User must only create direct subclasses of TypeToken, but not subclasses of subclasses (...) of
   * TypeToken.
   */
  @Test
  public void testTypeTokenSubSubClass() {
    class SubTypeToken<T> extends TypeToken<String> {}
    class SubSubTypeToken1<T> extends SubTypeToken<T> {}
    class SubSubTypeToken2 extends SubTypeToken<Integer> {}

    IllegalStateException e =
        assertThrows(IllegalStateException.class, () -> new SubTypeToken<Integer>() {});
    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");

    e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken1<Integer>());
    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");

    e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken2());
    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");
  }

  private static <M> void createTypeTokenTypeVariable() {
    var unused = new TypeToken<M>() {};
  }

  /**
   * TypeToken type argument must not contain a type variable because, due to type erasure, at
   * runtime only the bound of the type variable is available which is likely not what the user
   * wanted.
   *
   * <p>Note that type variables are allowed for the {@code TypeToken} factory methods calling
   * {@code TypeToken(Type)} because for them the return type is {@code TypeToken<?>} which does not
   * give a false sense of type-safety.
   */
  @Test
  public void testTypeTokenTypeVariable() throws Exception {
    // Put the test code inside generic class to be able to access `T`
    class Enclosing<T> {
      @SuppressWarnings("ClassCanBeStatic")
      class Inner {}

      void test() {
        String expectedMessage =
            "TypeToken type argument must not contain a type variable;"
                + " captured type variable T declared by "
                + Enclosing.class
                + "\n"
                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable";
        IllegalArgumentException e =
            assertThrows(IllegalArgumentException.class, () -> new TypeToken<T>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<List<List<T>>>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        e =
            assertThrows(
                IllegalArgumentException.class, () -> new TypeToken<List<? extends List<T>>>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        e =
            assertThrows(
                IllegalArgumentException.class, () -> new TypeToken<List<? super List<T>>>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<List<T>[]>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        e =
            assertThrows(
                IllegalArgumentException.class, () -> new TypeToken<Enclosing<T>.Inner>() {});
        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);

        String systemProperty = "gson.allowCapturingTypeVariables";
        try {
          // Any value other than 'true' should be ignored
          System.setProperty(systemProperty, "some-value");

          e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<T>() {});
          assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
        } finally {
          System.clearProperty(systemProperty);
        }

        try {
          System.setProperty(systemProperty, "true");

          TypeToken<?> typeToken = new TypeToken<T>() {};
          assertThat(typeToken.getType()).isEqualTo(Enclosing.class.getTypeParameters()[0]);
        } finally {
          System.clearProperty(systemProperty);
        }
      }

      <M> void testMethodTypeVariable() throws Exception {
        Method testMethod = Enclosing.class.getDeclaredMethod("testMethodTypeVariable");
        IllegalArgumentException e =
            assertThrows(IllegalArgumentException.class, () -> new TypeToken<M>() {});
        assertThat(e)
            .hasMessageThat()
            .isAnyOf(
                "TypeToken type argument must not contain a type variable;"
                    + " captured type variable M declared by "
                    + testMethod
                    + "\n"
                    + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable",
                // Note: When running this test in Eclipse IDE or with certain Java versions it
                // seems to capture `null` instead of the type variable, see
                // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/975
                "TypeToken captured `null` as type argument; probably a compiler / runtime bug");
      }
    }

    new Enclosing<>().test();
    new Enclosing<>().testMethodTypeVariable();

    Method testMethod = TypeTokenTest.class.getDeclaredMethod("createTypeTokenTypeVariable");
    IllegalArgumentException e =
        assertThrows(IllegalArgumentException.class, () -> createTypeTokenTypeVariable());
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "TypeToken type argument must not contain a type variable;"
                + " captured type variable M declared by "
                + testMethod
                + "\n"
                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable");

    // Using type variable as argument for factory methods should be allowed; this is not a
    // type-safety problem because the user would have to perform unsafe casts
    TypeVariable<?> typeVar = Enclosing.class.getTypeParameters()[0];
    TypeToken<?> typeToken = TypeToken.get(typeVar);
    assertThat(typeToken.getType()).isEqualTo(typeVar);

    TypeToken<?> parameterizedTypeToken = TypeToken.getParameterized(List.class, typeVar);
    ParameterizedType parameterizedType = (ParameterizedType) parameterizedTypeToken.getType();
    assertThat(parameterizedType.getRawType()).isEqualTo(List.class);
    assertThat(parameterizedType.getActualTypeArguments()).asList().containsExactly(typeVar);
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void testTypeTokenRaw() {
    IllegalStateException e = assertThrows(IllegalStateException.class, () -> new TypeToken() {});
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "TypeToken must be created with a type argument: new TypeToken<...>() {}; When using"
                + " code shrinkers (ProGuard, R8, ...) make sure that generic signatures are"
                + " preserved.\n"
                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#type-token-raw");
  }
}

// Have to declare these classes here as top-level classes because otherwise tests for
// TypeToken.getParameterized fail due to owner type mismatch
class GenericWithBound<T extends Number> {}

class GenericWithMultiBound<T extends Number & CharSequence> {}

@SuppressWarnings("serial")
abstract class ClassSatisfyingBounds extends Number implements CharSequence {}