Back to Repositories

Testing Protocol Buffer Annotation Processing in google/gson

This test suite validates Protocol Buffer functionality with annotations in the GSON library, focusing on serialization and deserialization of protobuf messages with custom field names and enum values. The tests verify proper handling of annotated proto messages, enum serialization formats, and various edge cases.

Test Coverage Overview

The test suite provides comprehensive coverage of Protocol Buffer annotation handling:

  • Custom field name serialization and deserialization
  • Enum value handling with different serialization formats
  • Nested message structure validation
  • Edge cases including missing fields and unknown enum values
  • Custom format conversion between different case styles

Implementation Analysis

The testing approach utilizes JUnit framework with a structured setup using multiple GSON configurations. The implementation focuses on three main test scenarios:

  • Standard annotation-based serialization
  • Enum number-based serialization
  • Custom field name formatting with lower-hyphen case

Technical Details

Testing tools and configuration:

  • JUnit test framework
  • GSON Builder with ProtoTypeAdapter
  • Custom annotation extensions (Annotations.serializedName, Annotations.serializedValue)
  • Truth assertion library for enhanced verification
  • Multiple GSON instances with different configurations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Thorough setup and initialization in @Before methods
  • Comprehensive error case handling
  • Clear test method naming conventions
  • Systematic validation of both serialization and deserialization
  • Explicit boundary testing for enum values

google/gson

proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithAnnotationsTest.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.protobuf.functional;

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

import com.google.common.base.CaseFormat;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.protobuf.ProtoTypeAdapter;
import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
import com.google.gson.protobuf.generated.Annotations;
import com.google.gson.protobuf.generated.Bag.OuterMessage;
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations;
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage;
import com.google.protobuf.GeneratedMessage;
import org.junit.Before;
import org.junit.Test;

/**
 * Functional tests for protocol buffers using annotations for field names and enum values.
 *
 * @author Emmanuel Cron
 */
public class ProtosWithAnnotationsTest {
  private Gson gson;
  private Gson gsonWithEnumNumbers;
  private Gson gsonWithLowerHyphen;

  @Before
  public void setUp() throws Exception {
    ProtoTypeAdapter.Builder protoTypeAdapter =
        ProtoTypeAdapter.newBuilder()
            .setEnumSerialization(EnumSerialization.NAME)
            .addSerializedNameExtension(Annotations.serializedName)
            .addSerializedEnumValueExtension(Annotations.serializedValue);
    gson =
        new GsonBuilder()
            .registerTypeHierarchyAdapter(GeneratedMessage.class, protoTypeAdapter.build())
            .create();
    gsonWithEnumNumbers =
        new GsonBuilder()
            .registerTypeHierarchyAdapter(
                GeneratedMessage.class,
                protoTypeAdapter.setEnumSerialization(EnumSerialization.NUMBER).build())
            .create();
    gsonWithLowerHyphen =
        new GsonBuilder()
            .registerTypeHierarchyAdapter(
                GeneratedMessage.class,
                protoTypeAdapter
                    .setFieldNameSerializationFormat(
                        CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_HYPHEN)
                    .build())
            .create();
  }

  @Test
  public void testProtoWithAnnotations_deserialize() {
    String json =
        String.format(
            "{  %n"
                + "   \"id\":\"41e5e7fd6065d101b97018a465ffff01\",%n"
                + "   \"expiration_date\":{  %n"
                + "      \"month\":\"12\",%n"
                + "      \"year\":\"2017\",%n"
                + "      \"timeStamp\":\"9864653135687\",%n"
                + "      \"countryCode5f55\":\"en_US\"%n"
                + "   },%n"
                // Don't define innerMessage1
                + "   \"innerMessage2\":{  %n"
                // Set a number as a string; it should work
                + "      \"nIdCt\":\"98798465\",%n"
                + "      \"content\":\"text/plain\",%n"
                + "      \"$binary_data$\":[  %n"
                + "         {  %n"
                + "            \"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\",%n"
                // Don't define width
                + "            \"height\":665%n"
                + "         },%n"
                + "         {  %n"
                // Define as an int; it should work
                + "            \"data\":65,%n"
                + "            \"width\":-56684%n"
                // Don't define height
                + "         }%n"
                + "      ]%n"
                + "   },%n"
                // Define a bunch of non recognizable data
                + "   \"non_existing\":\"foobar\",%n"
                + "   \"stillNot\":{  %n"
                + "      \"bunch\":\"of_useless data\"%n"
                + "   }%n"
                + "}");
    ProtoWithAnnotations proto = gson.fromJson(json, ProtoWithAnnotations.class);
    assertThat(proto.getId()).isEqualTo("41e5e7fd6065d101b97018a465ffff01");
    assertThat(proto.getOuterMessage())
        .isEqualTo(
            OuterMessage.newBuilder()
                .setMonth(12)
                .setYear(2017)
                .setLongTimestamp(9864653135687L)
                .setCountryCode5F55("en_US")
                .build());
    assertThat(proto.hasInnerMessage1()).isFalse();
    assertThat(proto.getInnerMessage2())
        .isEqualTo(
            InnerMessage.newBuilder()
                .setNIdCt(98798465)
                .setContent(InnerMessage.Type.TEXT)
                .addData(
                    InnerMessage.Data.newBuilder()
                        .setData("OFIN8e9fhwoeh8((⁹8efywoih")
                        .setHeight(665))
                .addData(InnerMessage.Data.newBuilder().setData("65").setWidth(-56684))
                .build());

    String rebuilt = gson.toJson(proto);
    assertThat(rebuilt)
        .isEqualTo(
            "{"
                + "\"id\":\"41e5e7fd6065d101b97018a465ffff01\","
                + "\"expiration_date\":{"
                + "\"month\":12,"
                + "\"year\":2017,"
                + "\"timeStamp\":9864653135687,"
                + "\"countryCode5f55\":\"en_US\""
                + "},"
                + "\"innerMessage2\":{"
                + "\"nIdCt\":98798465,"
                + "\"content\":\"text/plain\","
                + "\"$binary_data$\":["
                + "{"
                + "\"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\","
                + "\"height\":665"
                + "},"
                + "{"
                + "\"data\":\"65\","
                + "\"width\":-56684"
                + "}]}}");
  }

  @Test
  public void testProtoWithAnnotations_deserializeUnknownEnumValue() {
    String json = String.format("{  %n" + "   \"content\":\"UNKNOWN\"%n" + "}");
    InnerMessage proto = gson.fromJson(json, InnerMessage.class);
    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
  }

  @Test
  public void testProtoWithAnnotations_deserializeUnrecognizedEnumValue() {
    String json = String.format("{  %n" + "   \"content\":\"UNRECOGNIZED\"%n" + "}");
    var e = assertThrows(JsonParseException.class, () -> gson.fromJson(json, InnerMessage.class));
    assertThat(e).hasMessageThat().isEqualTo("Error while parsing proto");
    assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Unrecognized enum name: UNRECOGNIZED");
  }

  @Test
  public void testProtoWithAnnotations_deserializeWithEnumNumbers() {
    String json = String.format("{  %n" + "   \"content\":\"0\"%n" + "}");
    InnerMessage proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
    String rebuilt = gsonWithEnumNumbers.toJson(proto);
    assertThat(rebuilt).isEqualTo("{\"content\":0}");

    json = String.format("{  %n" + "   \"content\":\"2\"%n" + "}");
    proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.IMAGE);
    rebuilt = gsonWithEnumNumbers.toJson(proto);
    assertThat(rebuilt).isEqualTo("{\"content\":2}");
  }

  @Test
  public void testProtoWithAnnotations_deserializeUnrecognizedEnumNumber() {
    String json = String.format("{  %n" + "   \"content\":\"99\"%n" + "}");
    var e =
        assertThrows(
            JsonParseException.class, () -> gsonWithEnumNumbers.fromJson(json, InnerMessage.class));
    assertThat(e).hasMessageThat().isEqualTo("Error while parsing proto");
    assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Unrecognized enum value: 99");
  }

  @Test
  public void testProtoWithAnnotations_serialize() {
    ProtoWithAnnotations proto =
        ProtoWithAnnotations.newBuilder()
            .setId("09f3j20839h032y0329hf30932h0nffn")
            .setOuterMessage(
                OuterMessage.newBuilder()
                    .setMonth(14)
                    .setYear(6650)
                    .setLongTimestamp(468406876880768L))
            .setInnerMessage1(
                InnerMessage.newBuilder()
                    .setNIdCt(12)
                    .setContent(InnerMessage.Type.IMAGE)
                    .addData(InnerMessage.Data.newBuilder().setData("data$$").setWidth(200))
                    .addData(InnerMessage.Data.newBuilder().setHeight(56)))
            .build();

    String json = gsonWithLowerHyphen.toJson(proto);
    assertThat(json)
        .isEqualTo(
            "{\"id\":\"09f3j20839h032y0329hf30932h0nffn\","
                + "\"expiration_date\":{"
                + "\"month\":14,"
                + "\"year\":6650,"
                + "\"timeStamp\":468406876880768"
                + "},"
                // This field should be using hyphens
                + "\"inner-message-1\":{"
                + "\"n--id-ct\":12,"
                + "\"content\":2,"
                + "\"$binary_data$\":["
                + "{"
                + "\"data\":\"data$$\","
                + "\"width\":200"
                + "},"
                + "{"
                + "\"height\":56"
                + "}]"
                + "}"
                + "}");

    ProtoWithAnnotations rebuilt = gsonWithLowerHyphen.fromJson(json, ProtoWithAnnotations.class);
    assertThat(rebuilt).isEqualTo(proto);
  }
}