Repository: firebase/firebase-admin-java
Pull request #490 (2020-10-28 19:35:35)

Add Remote Config Parameter Group type
- Add Remote Config Parameter Group type
- Implement `equals()` in all public RC types
- Add unit tests for public RC types
- Refactor existing unit tests to use `equals()` when comparing templates

Related to: #446 

lahirumaramba: > LGTM
> 
> Nit: Unit tests use a lot of short variable names like `c` and `p`. That's not very Java idiomatic.

Thanks for pointing this out!! Updated with proper variable names!

Patch:
diff --git a/src/main/java/com/google/firebase/remoteconfig/Parameter.java b/src/main/java/com/google/firebase/remoteconfig/Parameter.java
index 7198e2a..d61816b 100644
--- a/src/main/java/com/google/firebase/remoteconfig/Parameter.java
+++ b/src/main/java/com/google/firebase/remoteconfig/Parameter.java
@@ -25,6 +25,7 @@ import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValue
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Represents a Remote Config parameter that can be included in a {@link Template}.
@@ -73,7 +74,7 @@ public final class Parameter {
   /**
    * Gets the description of the parameter.
    *
-   * @return The {@link String} description of the parameter or null.
+   * @return The description of the parameter or null.
    */
   @Nullable
   public String getDescription() {
@@ -144,4 +145,23 @@ public final class Parameter {
             .setDescription(description)
             .setConditionalValues(conditionalResponseValues);
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Parameter parameter = (Parameter) o;
+    return Objects.equals(defaultValue, parameter.defaultValue)
+            && Objects.equals(description, parameter.description)
+            && Objects.equals(conditionalValues, parameter.conditionalValues);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(defaultValue, description, conditionalValues);
+  }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterGroup.java b/src/main/java/com/google/firebase/remoteconfig/ParameterGroup.java
new file mode 100644
index 0000000..7246659
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/ParameterGroup.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.firebase.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import com.google.firebase.remoteconfig.internal.TemplateResponse;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterGroupResponse;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterResponse;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents a Remote Config parameter group that can be included in a {@link Template}.
+ * Grouping parameters is only for management purposes and does not affect client-side
+ * fetching of parameter values.
+ */
+public final class ParameterGroup {
+
+  private String description;
+  private Map<String, Parameter> parameters;
+
+  /**
+   * Creates a new {@link ParameterGroup}.
+   */
+  public ParameterGroup() {
+    parameters = new HashMap<>();
+  }
+
+  ParameterGroup(@NonNull ParameterGroupResponse parameterGroupResponse) {
+    checkNotNull(parameterGroupResponse);
+    this.parameters = new HashMap<>();
+    if (parameterGroupResponse.getParameters() != null) {
+      for (Map.Entry<String, TemplateResponse.ParameterResponse> entry
+              : parameterGroupResponse.getParameters().entrySet()) {
+        this.parameters.put(entry.getKey(), new Parameter(entry.getValue()));
+      }
+    }
+    this.description = parameterGroupResponse.getDescription();
+  }
+
+  /**
+   * Gets the description of the parameter group.
+   *
+   * @return The description of the parameter or null.
+   */
+  @Nullable
+  public String getDescription() {
+    return description;
+  }
+
+  /**
+   * Gets the map of parameters that belong to this group.
+   *
+   * @return A non-null map of parameter keys to their optional default values and optional
+   *     conditional values.
+   */
+  @NonNull
+  public Map<String, Parameter> getParameters() {
+    return parameters;
+  }
+
+  /**
+   * Sets the description of the parameter group.
+   * Should not be over 256 characters and may contain any Unicode characters.
+   *
+   * @param description The description of the parameter group.
+   * @return This {@link ParameterGroup}.
+   */
+  public ParameterGroup setDescription(@Nullable String description) {
+    this.description = description;
+    return this;
+  }
+
+  /**
+   * Sets the map of parameters that belong to this group.
+   *
+   * <p>A parameter only appears once per Remote Config template.
+   * An ungrouped parameter appears at the top level, whereas a
+   * parameter organized within a group appears within its group's map of parameters.
+   *
+   * @param parameters A non-null map of parameter keys to their optional default values and
+   *                   optional conditional values.
+   * @return This {@link ParameterGroup} instance.
+   */
+  public ParameterGroup setParameters(
+          @NonNull Map<String, Parameter> parameters) {
+    checkNotNull(parameters, "parameters must not be null.");
+    this.parameters = parameters;
+    return this;
+  }
+
+  ParameterGroupResponse toParameterGroupResponse() {
+    Map<String, ParameterResponse> parameterResponses = new HashMap<>();
+    for (Map.Entry<String, Parameter> entry : this.parameters.entrySet()) {
+      parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse());
+    }
+    return new ParameterGroupResponse()
+            .setDescription(this.description)
+            .setParameters(parameterResponses);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    ParameterGroup that = (ParameterGroup) o;
+    return Objects.equals(description, that.description)
+            && Objects.equals(parameters, that.parameters);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(description, parameters);
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
index b2a182e..90bf0e5 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
@@ -21,6 +21,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse;
 
+import java.util.Objects;
+
 /**
  * Represents a Remote Config parameter value that can be used in a {@link Template}.
  */
@@ -57,7 +59,7 @@ public abstract class ParameterValue {
   }
 
   /**
-   * Represents an explicit Remote Config parameter value with a {@link String} value that the
+   * Represents an explicit Remote Config parameter value with a value that the
    * parameter is set to.
    */
   public static final class Explicit extends ParameterValue {
@@ -71,7 +73,7 @@ public abstract class ParameterValue {
     /**
      * Gets the value of {@link ParameterValue.Explicit}.
      *
-     * @return The {@link String} value.
+     * @return The value.
      */
     public String getValue() {
       return this.value;
@@ -82,6 +84,23 @@ public abstract class ParameterValue {
       return new ParameterValueResponse()
               .setValue(this.value);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Explicit explicit = (Explicit) o;
+      return Objects.equals(value, explicit.value);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(value);
+    }
   }
 
   /**
@@ -93,5 +112,16 @@ public abstract class ParameterValue {
     ParameterValueResponse toParameterValueResponse() {
       return new ParameterValueResponse().setUseInAppDefault(true);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      return true;
+    }
   }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java
index 902273a..f54cc80 100644
--- a/src/main/java/com/google/firebase/remoteconfig/Template.java
+++ b/src/main/java/com/google/firebase/remoteconfig/Template.java
@@ -25,6 +25,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Represents a Remote Config template.
@@ -34,6 +35,7 @@ public final class Template {
   private String etag;
   private Map<String, Parameter> parameters;
   private List<Condition> conditions;
+  private Map<String, ParameterGroup> parameterGroups;
 
   /**
    * Creates a new {@link Template}.
@@ -41,12 +43,14 @@ public final class Template {
   public Template() {
     parameters = new HashMap<>();
     conditions = new ArrayList<>();
+    parameterGroups = new HashMap<>();
   }
 
   Template(@NonNull TemplateResponse templateResponse) {
     checkNotNull(templateResponse);
     this.parameters = new HashMap<>();
     this.conditions = new ArrayList<>();
+    this.parameterGroups = new HashMap<>();
     if (templateResponse.getParameters() != null) {
       for (Map.Entry<String, TemplateResponse.ParameterResponse> entry
               : templateResponse.getParameters().entrySet()) {
@@ -59,6 +63,12 @@ public final class Template {
         this.conditions.add(new Condition(conditionResponse));
       }
     }
+    if (templateResponse.getParameterGroups() != null) {
+      for (Map.Entry<String, TemplateResponse.ParameterGroupResponse> entry
+              : templateResponse.getParameterGroups().entrySet()) {
+        this.parameterGroups.put(entry.getKey(), new ParameterGroup(entry.getValue()));
+      }
+    }
   }
 
   /**
@@ -84,13 +94,23 @@ public final class Template {
   /**
    * Gets the list of conditions of the template.
    *
-   * @return A non-null list of conditions
+   * @return A non-null list of conditions.
    */
   @NonNull
   public List<Condition> getConditions() {
     return conditions;
   }
 
+  /**
+   * Gets the map of parameter groups of the template.
+   *
+   * @return A non-null map of parameter group names to their parameter group instances.
+   */
+  @NonNull
+  public Map<String, ParameterGroup> getParameterGroups() {
+    return parameterGroups;
+  }
+
   /**
    * Sets the map of parameters of the template.
    *
@@ -118,6 +138,20 @@ public final class Template {
     return this;
   }
 
+  /**
+   * Sets the map of parameter groups of the template.
+   *
+   * @param parameterGroups A non-null map of parameter group names to their
+   *                        parameter group instances.
+   * @return This {@link Template} instance.
+   */
+  public Template setParameterGroups(
+          @NonNull Map<String, ParameterGroup> parameterGroups) {
+    checkNotNull(parameterGroups, "parameter groups must not be null.");
+    this.parameterGroups = parameterGroups;
+    return this;
+  }
+
   Template setETag(String etag) {
     this.etag = etag;
     return this;
@@ -132,8 +166,33 @@ public final class Template {
     for (Condition condition : this.conditions) {
       conditionResponses.add(condition.toConditionResponse());
     }
+    Map<String, TemplateResponse.ParameterGroupResponse> parameterGroupResponse = new HashMap<>();
+    for (Map.Entry<String, ParameterGroup> entry : this.parameterGroups.entrySet()) {
+      parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse());
+    }
     return new TemplateResponse()
             .setParameters(parameterResponses)
-            .setConditions(conditionResponses);
+            .setConditions(conditionResponses)
+            .setParameterGroups(parameterGroupResponse);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Template template = (Template) o;
+    return Objects.equals(etag, template.etag)
+            && Objects.equals(parameters, template.parameters)
+            && Objects.equals(conditions, template.conditions)
+            && Objects.equals(parameterGroups, template.parameterGroups);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(etag, parameters, conditions, parameterGroups);
   }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
index cd1463e..1995362 100644
--- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
+++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
@@ -33,6 +33,9 @@ public final class TemplateResponse {
   @Key("conditions")
   private List<ConditionResponse> conditions;
 
+  @Key("parameterGroups")
+  private Map<String, ParameterGroupResponse> parameterGroups;
+
   public Map<String, ParameterResponse> getParameters() {
     return parameters;
   }
@@ -41,6 +44,10 @@ public final class TemplateResponse {
     return conditions;
   }
 
+  public Map<String, ParameterGroupResponse> getParameterGroups() {
+    return parameterGroups;
+  }
+
   public TemplateResponse setParameters(
           Map<String, ParameterResponse> parameters) {
     this.parameters = parameters;
@@ -53,6 +60,12 @@ public final class TemplateResponse {
     return this;
   }
 
+  public TemplateResponse setParameterGroups(
+          Map<String, ParameterGroupResponse> parameterGroups) {
+    this.parameterGroups = parameterGroups;
+    return this;
+  }
+
   /**
    * The Data Transfer Object for parsing Remote Config parameter responses from the
    * Remote Config service.
@@ -171,4 +184,36 @@ public final class TemplateResponse {
       return this;
     }
   }
+
+  /**
+   * The Data Transfer Object for parsing Remote Config parameter groups responses from the
+   * Remote Config service.
+   **/
+  public static final class ParameterGroupResponse {
+
+    @Key("description")
+    private String description;
+
+    @Key("parameters")
+    private Map<String, ParameterResponse> parameters;
+
+    public Map<String, ParameterResponse> getParameters() {
+      return parameters;
+    }
+
+    public String getDescription() {
+      return description;
+    }
+
+    public ParameterGroupResponse setParameters(
+            Map<String, ParameterResponse> parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    public ParameterGroupResponse setDescription(String description) {
+      this.description = description;
+      return this;
+    }
+  }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java
index 1c8b8cd..aba01ea 100644
--- a/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/ConditionTest.java
@@ -18,11 +18,64 @@ package com.google.firebase.remoteconfig;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 
 public class ConditionTest {
 
+  @Test
+  public void testConstructor() {
+    Condition condition = new Condition("ios_en_1", "expression1");
+
+    assertEquals("ios_en_1", condition.getName());
+    assertEquals("expression1", condition.getExpression());
+    assertNull(condition.getTagColor());
+  }
+
+  @Test
+  public void testConstructorWithColor() {
+    Condition condition = new Condition("ios_en_2", "expression2", TagColor.BLUE);
+
+    assertEquals("ios_en_2", condition.getName());
+    assertEquals("expression2", condition.getExpression());
+    assertEquals(TagColor.BLUE, condition.getTagColor());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testIllegalConstructor() {
+    new Condition(null, null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorWithNullConditionResponse() {
+    new Condition(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testSetNullName() {
+    Condition condition = new Condition("ios", "exp");
+    condition.setName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testSetEmptyName() {
+    Condition condition = new Condition("ios", "exp");
+    condition.setName("");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testSetNullExpression() {
+    Condition condition = new Condition("ios", "exp");
+    condition.setExpression(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testSetEmptyExpression() {
+    Condition condition = new Condition("ios", "exp");
+    condition.setExpression("");
+  }
+
   @Test
   public void testEquality() {
     final Condition conditionOne = new Condition("ios", "device.os == 'ios'", TagColor.GREEN);
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
index aabc98a..910cc04 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
@@ -84,33 +84,27 @@ public class FirebaseRemoteConfigClientImplTest {
     response.addHeader("etag", TEST_ETAG);
     response.setContent(MOCK_TEMPLATE_RESPONSE);
 
-    Template template = client.getTemplate();
-
-    // Check Parameters
-    assertEquals(TEST_ETAG, template.getETag());
-    Map<String, Parameter> parameters = template.getParameters();
-    assertEquals(2, parameters.size());
-    assertTrue(parameters.containsKey("welcome_message_text"));
-    Parameter welcomeMessageParameter = parameters.get("welcome_message_text");
-    assertEquals("text for welcome message!", welcomeMessageParameter.getDescription());
-    ParameterValue.Explicit explicitDefaultValue =
-            (ParameterValue.Explicit) welcomeMessageParameter.getDefaultValue();
-    assertEquals("welcome to app", explicitDefaultValue.getValue());
-    Map<String, ParameterValue> conditionalValues = welcomeMessageParameter
-            .getConditionalValues();
-    assertEquals(1, conditionalValues.size());
-    assertTrue(conditionalValues.containsKey("ios_en"));
-    ParameterValue.Explicit value =
-            (ParameterValue.Explicit) conditionalValues.get("ios_en");
-    assertEquals("welcome to app en", value.getValue());
-    assertTrue(parameters.containsKey("header_text"));
-    Parameter headerParameter = parameters.get("header_text");
-    assertTrue(
-            headerParameter.getDefaultValue() instanceof ParameterValue.InAppDefault);
-    checkGetRequestHeader(interceptor.getLastRequest());
-
-    // Check Conditions
-    List<Condition> actualConditions = template.getConditions();
+    Template receivedTemplate = client.getTemplate();
+    Map<String, Parameter> expectedParameters = ImmutableMap.of(
+            "welcome_message_text", new Parameter()
+                    .setDefaultValue(ParameterValue.of("welcome to app"))
+                    .setConditionalValues(ImmutableMap.<String, ParameterValue>of(
+                            "ios_en", ParameterValue.of("welcome to app en")
+                    ))
+                    .setDescription("text for welcome message!"),
+            "header_text", new Parameter()
+                    .setDefaultValue(ParameterValue.inAppDefault())
+    );
+    Map<String, ParameterGroup> expectedParameterGroups = ImmutableMap.of(
+            "new menu", new ParameterGroup()
+                    .setDescription("New Menu")
+                    .setParameters(ImmutableMap.of(
+                            "pumpkin_spice_season", new Parameter()
+                                    .setDefaultValue(ParameterValue.of("true"))
+                                    .setDescription("Whether it's currently pumpkin spice season.")
+                            )
+                    )
+    );
     List<Condition> expectedConditions = ImmutableList.of(
             new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']")
                     .setTagColor(TagColor.INDIGO),
@@ -118,10 +112,15 @@ public class FirebaseRemoteConfigClientImplTest {
                     "device.os == 'android' && device.country in ['us', 'uk']")
                     .setTagColor(TagColor.UNSPECIFIED)
     );
-    assertEquals(expectedConditions.size(), actualConditions.size());
-    for (int i = 0; i < expectedConditions.size(); i++) {
-      assertEquals(expectedConditions.get(i), actualConditions.get(i));
-    }
+    Template expectedTemplate = new Template()
+            .setParameters(expectedParameters)
+            .setParameterGroups(expectedParameterGroups)
+            .setConditions(expectedConditions)
+            .setETag(TEST_ETAG);
+
+    assertEquals(TEST_ETAG, receivedTemplate.getETag());
+    assertEquals(expectedTemplate, receivedTemplate);
+    checkGetRequestHeader(interceptor.getLastRequest());
   }
 
   @Test
diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterGroupTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterGroupTest.java
new file mode 100644
index 0000000..8a73441
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ParameterGroupTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ParameterGroupTest {
+
+  @Test
+  public void testConstructor() {
+    final ParameterGroup parameterGroup = new ParameterGroup();
+
+    assertNotNull(parameterGroup.getParameters());
+    assertTrue(parameterGroup.getParameters().isEmpty());
+    assertNull(parameterGroup.getDescription());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorWithNullParameterGroupResponse() {
+    new ParameterGroup(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testSetNullParameters() {
+    ParameterGroup parameterGroup = new ParameterGroup();
+    parameterGroup.setParameters(null);
+  }
+
+  @Test
+  public void testEquality() {
+    final ParameterGroup parameterGroupOne = new ParameterGroup();
+    final ParameterGroup parameterGroupTwo = new ParameterGroup();
+
+    assertEquals(parameterGroupOne, parameterGroupTwo);
+
+    final ParameterGroup parameterGroupThree = new ParameterGroup()
+            .setDescription("description");
+    final ParameterGroup parameterGroupFour = new ParameterGroup()
+            .setDescription("description");
+
+    assertEquals(parameterGroupThree, parameterGroupFour);
+
+    final Map<String, Parameter> parameters = ImmutableMap.of(
+            "header_text", new Parameter().setDefaultValue(ParameterValue.of("Welcome")),
+            "promo", new Parameter()
+                    .setDefaultValue(ParameterValue.inAppDefault())
+                    .setConditionalValues(ImmutableMap.<String, ParameterValue>of(
+                            "ios", ParameterValue.of("ios header text")
+                    ))
+    );
+    final ParameterGroup parameterGroupFive = new ParameterGroup()
+            .setDescription("description")
+            .setParameters(parameters);
+    final ParameterGroup parameterGroupSix = new ParameterGroup()
+            .setDescription("description")
+            .setParameters(parameters);
+
+    assertEquals(parameterGroupFive, parameterGroupSix);
+    assertNotEquals(parameterGroupOne, parameterGroupThree);
+    assertNotEquals(parameterGroupOne, parameterGroupFive);
+    assertNotEquals(parameterGroupThree, parameterGroupFive);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java
new file mode 100644
index 0000000..952ea8b
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ParameterTest {
+
+  @Test
+  public void testConstructor() {
+    final Parameter parameter = new Parameter();
+
+    assertNotNull(parameter.getConditionalValues());
+    assertTrue(parameter.getConditionalValues().isEmpty());
+    assertNull(parameter.getDefaultValue());
+    assertNull(parameter.getDescription());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorWithNullParameterResponse() {
+    new Parameter(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testSetNullConditionalValues() {
+    Parameter parameter = new Parameter();
+    parameter.setConditionalValues(null);
+  }
+
+  @Test
+  public void testEquality() {
+    final Parameter parameterOne = new Parameter()
+            .setDefaultValue(ParameterValue.of("hello"));
+    final Parameter parameterTwo = new Parameter()
+            .setDefaultValue(ParameterValue.of("hello"));
+
+    assertEquals(parameterOne, parameterTwo);
+
+    final Parameter parameterThree = new Parameter()
+            .setDefaultValue(ParameterValue.inAppDefault())
+            .setDescription("greeting text");
+    final Parameter parameterFour = new Parameter()
+            .setDefaultValue(ParameterValue.inAppDefault())
+            .setDescription("greeting text");
+
+    assertEquals(parameterThree, parameterFour);
+
+    final Map<String, ParameterValue> conditionalValues = ImmutableMap.of(
+            "ios", ParameterValue.of("hello ios"),
+            "android", ParameterValue.of("hello android"),
+            "promo", ParameterValue.inAppDefault()
+    );
+    final Parameter parameterFive = new Parameter()
+            .setDefaultValue(ParameterValue.inAppDefault())
+            .setDescription("greeting text")
+            .setConditionalValues(conditionalValues);
+    final Parameter parameterSix = new Parameter()
+            .setDefaultValue(ParameterValue.inAppDefault())
+            .setDescription("greeting text")
+            .setConditionalValues(conditionalValues);
+
+    assertEquals(parameterFive, parameterSix);
+    assertNotEquals(parameterOne, parameterThree);
+    assertNotEquals(parameterOne, parameterFive);
+    assertNotEquals(parameterThree, parameterFive);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
new file mode 100644
index 0000000..842fd80
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import org.junit.Test;
+
+public class ParameterValueTest {
+
+  @Test
+  public void testCreateExplicitValue() {
+    final ParameterValue.Explicit parameterValue = ParameterValue.of("title text");
+
+    assertEquals("title text", parameterValue.getValue());
+  }
+
+  @Test
+  public void testCreateInAppDefault() {
+    final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault();
+
+    assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass());
+  }
+
+  @Test
+  public void testEquality() {
+    ParameterValue.Explicit parameterValueOne = ParameterValue.of("value");
+    ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value");
+    ParameterValue.Explicit parameterValueThree = ParameterValue.of("title");
+
+    assertEquals(parameterValueOne, parameterValueTwo);
+    assertNotEquals(parameterValueOne, parameterValueThree);
+
+    ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault();
+    ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault();
+
+    assertEquals(parameterValueFour, parameterValueFive);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java
new file mode 100644
index 0000000..ce27230
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class TemplateTest {
+
+  @Test
+  public void testConstructor() {
+    Template template = new Template();
+
+    assertNotNull(template.getParameters());
+    assertNotNull(template.getConditions());
+    assertNotNull(template.getParameterGroups());
+    assertTrue(template.getParameters().isEmpty());
+    assertTrue(template.getConditions().isEmpty());
+    assertTrue(template.getParameterGroups().isEmpty());
+    assertNull(template.getETag());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testConstructorWithNullTemplateResponse() {
+    new Template(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testSetNullParameters() {
+    Template template = new Template();
+    template.setParameters(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testSetNullConditions() {
+    Template template = new Template();
+    template.setConditions(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testSetNullParameterGroups() {
+    Template template = new Template();
+    template.setParameterGroups(null);
+  }
+
+  @Test
+  public void testEquality() {
+    final Template templateOne = new Template();
+    final Template templateTwo = new Template();
+
+    assertEquals(templateOne, templateTwo);
+
+    final List<Condition> conditions = ImmutableList.<Condition>of(
+            new Condition("ios_en", "exp ios")
+                    .setTagColor(TagColor.INDIGO),
+            new Condition("android_en", "exp android")
+    );
+    final Map<String, ParameterValue> conditionalValues = ImmutableMap.of(
+            "ios", ParameterValue.of("hello ios"),
+            "android", ParameterValue.of("hello android"),
+            "promo", ParameterValue.inAppDefault()
+    );
+    final Map<String, Parameter> parameters = ImmutableMap.of(
+            "greeting_header", new Parameter()
+                    .setDefaultValue(ParameterValue.inAppDefault())
+                    .setDescription("greeting header text")
+                    .setConditionalValues(conditionalValues),
+            "greeting_text", new Parameter()
+                    .setDefaultValue(ParameterValue.inAppDefault())
+                    .setDescription("greeting text")
+                    .setConditionalValues(conditionalValues)
+    );
+    final Template templateThree = new Template()
+            .setConditions(conditions)
+            .setParameters(parameters);
+    final Template templateFour = new Template()
+            .setConditions(conditions)
+            .setParameters(parameters);
+
+    assertEquals(templateThree, templateFour);
+
+    final Map<String, ParameterGroup> parameterGroups = ImmutableMap.of(
+            "greetings_group", new ParameterGroup()
+                    .setDescription("description")
+                    .setParameters(parameters)
+    );
+    final Template templateFive = new Template()
+            .setConditions(conditions)
+            .setParameters(parameters)
+            .setParameterGroups(parameterGroups);
+    final Template templateSix = new Template()
+            .setConditions(conditions)
+            .setParameters(parameters)
+            .setParameterGroups(parameterGroups);
+
+    assertEquals(templateFive, templateSix);
+
+    final Template templateSeven = new Template()
+            .setETag("etag-123456789097-20");
+    final Template templateEight = new Template()
+            .setETag("etag-123456789097-20");
+
+    assertEquals(templateSeven, templateEight);
+    assertNotEquals(templateOne, templateThree);
+    assertNotEquals(templateOne, templateFive);
+    assertNotEquals(templateOne, templateSeven);
+    assertNotEquals(templateThree, templateFive);
+    assertNotEquals(templateThree, templateSeven);
+    assertNotEquals(templateFive, templateSeven);
+  }
+}
diff --git a/src/test/resources/getRemoteConfig.json b/src/test/resources/getRemoteConfig.json
index df54117..8b0f664 100644
--- a/src/test/resources/getRemoteConfig.json
+++ b/src/test/resources/getRemoteConfig.json
@@ -28,7 +28,19 @@
       }
     }
   },
-  "parameterGroups": {},
+  "parameterGroups": {
+    "new menu": {
+      "description": "New Menu",
+      "parameters": {
+        "pumpkin_spice_season": {
+          "defaultValue": {
+            "value": "true"
+          },
+          "description": "Whether it's currently pumpkin spice season."
+        }
+      }
+    }
+  },
   "version": {
     "versionNumber": "17",
     "updateOrigin": "ADMIN_SDK_NODE",