Back to Repositories

Validating View Binding Inheritance in ButterKnife

This test suite validates the Butterknife view binding functionality when dealing with inheritance and classpath dependencies. It ensures proper generation of binding code for views and resources across parent-child class relationships.

Test Coverage Overview

The test suite provides comprehensive coverage of Butterknife’s view binding capabilities in inheritance scenarios.

Key areas tested include:
  • Parent binding generation from classpath classes
  • Indirect view binding requirements in constructors
  • Resource binding without view dependencies
  • Inheritance chain binding validation

Implementation Analysis

The testing approach uses JUnit with custom compilation testing utilities to verify generated binding code. It employs temporary folder management and custom classloaders to simulate runtime environments and validate binding generation across different class hierarchies.

Key patterns include:
  • Dynamic source compilation and verification
  • Classpath manipulation for inheritance testing
  • Generated source code validation

Technical Details

Testing tools and configuration:
  • JUnit test framework with @Rule and @Test annotations
  • Google Testing Compile utilities for source verification
  • JavaFileObjects for source code manipulation
  • Custom URLClassLoader for classpath testing
  • TemporaryFolder for test file management

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through thorough validation of edge cases and comprehensive coverage of inheritance scenarios.

Notable practices include:
  • Isolated test environments using temporary folders
  • Proper resource cleanup with try-with-resources
  • Comprehensive assertion validation
  • Clear test case organization and naming

jakewharton/butterknife

butterknife-runtime/src/test/java/butterknife/ClasspathParentBindTest.java

            
package butterknife;

import com.google.common.collect.ImmutableList;
import com.google.testing.compile.JavaFileObjects;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Locale;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import butterknife.compiler.ButterKnifeProcessor;

import static com.google.common.truth.Truth.assertAbout;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;
import static java.nio.charset.StandardCharsets.UTF_8;

/** Tests binding generation when superclasses are from classpath.  */
public class ClasspathParentBindTest {
  @Rule
  public TemporaryFolder tmp = new TemporaryFolder();

  @Test
  public void parentBindingInClasspath() throws IOException {
    JavaFileObject baseClassSource = JavaFileObjects.forSourceString("test.Test", ""
        + "package test;\n"
        + "import android.view.View;\n"
        + "import butterknife.BindView;\n"
        + "import butterknife.OnClick;\n"
        + "import butterknife.OnLongClick;\n"
        + "public class Test {\n"
        + "  @BindView(1) View view;\n"
        + "  @BindView(2) View view2;\n"
        + "  @OnClick(1) void doStuff() {}\n"
        + "  @OnLongClick(1) boolean doMoreStuff() { return false; }\n"
        + "}"
    );

    JavaFileObject subClassSource = JavaFileObjects.forSourceString("test.SubClass", ""
        + "package test;\n"
        + "import android.view.View;\n"
        + "import butterknife.BindView;\n"
        + "public class SubClass extends Test {\n"
        + "  @BindView(3) View view3;\n"
        + "}"
    );

    JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/SubClass_ViewBinding", ""
        + "// Generated code from Butter Knife. Do not modify!\n"
        + "package test;\n"
        + "import android.view.View;\n"
        + "import androidx.annotation.UiThread;\n"
        + "import butterknife.internal.Utils;\n"
        + "import java.lang.IllegalStateException;\n"
        + "import java.lang.Override;\n"
        + "public class SubClass_ViewBinding extends Test_ViewBinding {\n"
        + "  private SubClass target;\n"
        + "  @UiThread\n"
        + "  public SubClass_ViewBinding(SubClass target, View source) {\n"
        + "    super(target, source);\n"
        + "    this.target = target;\n"
        + "    target.view3 = Utils.findRequiredView(source, 3, \"field 'view3'\");\n"
        + "  }\n"
        + "  @Override\n"
        + "  public void unbind() {\n"
        + "    SubClass target = this.target;\n"
        + "    if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n"
        + "    this.target = null;\n"
        + "    target.view3 = null;\n"
        + "    super.unbind();\n"
        + "  }\n"
        + "}"
    );

    File classesOut = tmp.newFolder("classes-output");
    File sourcesOut = tmp.newFolder("sources-output");
    compileSources(classesOut, sourcesOut, baseClassSource);

    try (URLClassLoader compilationClasspath = new URLClassLoader(
        new URL[]{classesOut.toURI().toURL()}, this.getClass().getClassLoader())) {
      assertAbout(javaSource()).that(subClassSource)
          .withCompilerOptions("-Xlint:-processing")
          .withClasspathFrom(compilationClasspath)
          .processedWith(new ButterKnifeProcessor())
          .compilesWithoutWarnings()
          .and()
          .generatesSources(bindingSource);
    }
  }

  @Test
  public void indirectViewRequiredInConstructor() throws IOException {
    JavaFileObject classpathClass = JavaFileObjects.forSourceString("test.Test", ""
        + "package test;\n"
        + "import android.view.View;\n"
        + "import butterknife.BindView;\n"
        + "import butterknife.OnClick;\n"
        + "import butterknife.OnLongClick;\n"
        + "public class Test {\n"
        + "  @BindView(1) View view;\n"
        + "  @BindView(2) View view2;\n"
        + "  @OnClick(1) void doStuff() {}\n"
        + "  @OnLongClick(1) boolean doMoreStuff() { return false; }\n"
        + "}"
    );

    JavaFileObject subclassInClasspath = JavaFileObjects.forSourceString("test.SubClassTest", ""
        + "package test;\n"
        + "import butterknife.BindFloat;\n"
        + "public class SubClassTest extends Test{\n"
        + "  @BindFloat(1) float value;\n"
        + "}"
    );

    JavaFileObject toProcessSource = JavaFileObjects.forSourceString("test.ToProcess", ""
        + "package test;\n"
        + "import android.view.View;\n"
        + "import butterknife.BindView;\n"
        + "public class ToProcess extends SubClassTest {\n"
        + "  @BindView(3) View view3;\n"
        + "}"
    );

    JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/ToProcess_ViewBinding", ""
        + "// Generated code from Butter Knife. Do not modify!\n"
        + "package test;\n"
        + "import android.view.View;\n"
        + "import androidx.annotation.UiThread;\n"
        + "import butterknife.internal.Utils;\n"
        + "import java.lang.IllegalStateException;\n"
        + "import java.lang.Override;\n"
        + "public class ToProcess_ViewBinding extends SubClassTest_ViewBinding {\n"
        + "  private ToProcess target;\n"
        + "  @UiThread\n"
        + "  public ToProcess_ViewBinding(ToProcess target, View source) {\n"
        + "    super(target, source);\n"
        + "    this.target = target;\n"
        + "    target.view3 = Utils.findRequiredView(source, 3, \"field 'view3'\");\n"
        + "  }\n"
        + "  @Override\n"
        + "  public void unbind() {\n"
        + "    ToProcess target = this.target;\n"
        + "    if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n"
        + "    this.target = null;\n"
        + "    target.view3 = null;\n"
        + "    super.unbind();\n"
        + "  }\n"
        + "}"
    );

    File classesOut = tmp.newFolder("classes-output");
    File sourcesOut = tmp.newFolder("sources-output");
    compileSources(classesOut, sourcesOut, classpathClass, subclassInClasspath);

    try (URLClassLoader compilationClasspath = new URLClassLoader(
        new URL[]{classesOut.toURI().toURL()}, this.getClass().getClassLoader())) {
      assertAbout(javaSource()).that(toProcessSource)
          .withCompilerOptions("-Xlint:-processing")
          .withClasspathFrom(compilationClasspath)
          .processedWith(new ButterKnifeProcessor())
          .compilesWithoutWarnings()
          .and()
          .generatesSources(bindingSource);
    }
  }

  @Test
  public void viewNotRequiredInConstructor() throws IOException {
    JavaFileObject baseClass = JavaFileObjects.forSourceString("test.Test", ""
        + "package test;\n"
        + "import butterknife.BindFloat;\n"
        + "public class Test {\n"
        + "  @BindFloat(1) float one;\n"
        + "}"
    );

    JavaFileObject subClassSource = JavaFileObjects.forSourceString("test.SubClass", ""
        + "package test;\n"
        + "import butterknife.BindFloat;\n"
        + "public class SubClass extends Test {\n"
        + "  @BindFloat(2) float two;\n"
        + "}"
    );

    JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/SubClass_ViewBinding", ""
        + "// Generated code from Butter Knife. Do not modify!\n"
        + "package test;\n"
        + "import android.content.Context;\n"
        + "import android.view.View;\n"
        + "import androidx.annotation.UiThread;\n"
        + "import butterknife.internal.Utils;\n"
        + "import java.lang.Deprecated;\n"
        + "import java.lang.SuppressWarnings;\n"
        + "public class SubClass_ViewBinding extends Test_ViewBinding {\n"
        + "  /**\n"
        + "   * @deprecated Use {@link #SubClass_ViewBinding(SubClass, Context)} for direct creation.\n"
        + "   *     Only present for runtime invocation through {@code ButterKnife.bind()}.\n"
        + "   */\n"
        + "  @Deprecated\n"
        + "  @UiThread\n"
        + "  public SubClass_ViewBinding(SubClass target, View source) {\n"
        + "    this(target, source.getContext());\n"
        + "  }\n"
        + "  @UiThread\n"
        + "  @SuppressWarnings(\"ResourceType\")\n"
        + "  public SubClass_ViewBinding(SubClass target, Context context) {\n"
        + "    super(target, context);\n"
        + "    target.two = Utils.getFloat(context, 2);\n"
        + "  }\n"
        + "}"
    );

    File classesOut = tmp.newFolder("classes-output");
    File sourcesOut = tmp.newFolder("sources-output");
    compileSources(classesOut, sourcesOut, baseClass);

    try (URLClassLoader compilationClasspath = new URLClassLoader(
        new URL[]{classesOut.toURI().toURL()}, this.getClass().getClassLoader())) {
      assertAbout(javaSource()).that(subClassSource)
          .withCompilerOptions("-Xlint:-processing")
          .withClasspathFrom(compilationClasspath)
          .processedWith(new ButterKnifeProcessor())
          .compilesWithoutWarnings()
          .and()
          .generatesSources(bindingSource);
    }
  }

  private void compileSources(File classesOut, File sourcesOut, JavaFileObject... sources) {
    JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
    try {
      try (StandardJavaFileManager fileManager =
               javaCompiler.getStandardFileManager(null, Locale.getDefault(), UTF_8)) {
        JavaCompiler.CompilationTask javaCompilerTask = javaCompiler.getTask(null,
            fileManager,
            null,
            ImmutableList.of("-d", classesOut.getCanonicalPath(), "-s", sourcesOut.getCanonicalPath()),
            ImmutableList.of(),
            Arrays.asList(sources));
        javaCompilerTask.setProcessors(ImmutableList.of(new ButterKnifeProcessor()));
        javaCompilerTask.call();
      }
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }
}