Back to Repositories

Validating OnClick Event Handling in ButterKnife

This test suite validates the OnClick functionality in ButterKnife, an Android view binding library. It covers various aspects of click event handling, including simple clicks, multiple bindings, visibility modifiers, and argument casting for view interactions.

Test Coverage Overview

The test suite provides comprehensive coverage of ButterKnife’s OnClick annotation functionality:

  • Simple click event binding and unbinding
  • Multiple click handlers on single view
  • Different visibility modifiers (public, package, protected)
  • Multiple view ID bindings
  • Optional view binding scenarios
  • View argument type casting

Implementation Analysis

The testing approach utilizes JUnit and Android Instrumentation framework to validate click interactions. Tests are structured to verify both positive and negative scenarios, with explicit validation of binding lifecycle and event handling. The implementation leverages runOnMainSync for UI thread operations and custom ViewTree creation for controlled testing environments.

Technical Details

Testing tools and configuration:

  • JUnit test framework
  • Android Instrumentation Registry
  • Custom ViewTree test utility
  • ButterKnife binding and unbinding
  • UI thread synchronization
  • View hierarchy simulation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolation of test cases with proper setup and teardown
  • Comprehensive edge case coverage
  • Thread-safe UI testing with proper synchronization
  • Clear test method naming and organization
  • Proper resource cleanup with unbinding
  • Type-safe view casting validation

jakewharton/butterknife

butterknife-integration-test/src/androidTest/java/com/example/butterknife/functional/OnClickTest.java

            
package com.example.butterknife.functional;

import android.app.Instrumentation;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.test.InstrumentationRegistry;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Optional;
import butterknife.Unbinder;
import com.example.butterknife.BuildConfig;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assume.assumeFalse;

@SuppressWarnings("unused") // Used reflectively / by code gen.
public final class OnClickTest {
  private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

  static final class Simple {
    int clicks = 0;

    @OnClick(1) void click() {
      clicks++;
    }
  }

  @Test public void simple() {
    View tree = ViewTree.create(1);
    View view1 = tree.findViewById(1);

    Simple target = new Simple();
    Unbinder unbinder = ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertEquals(1, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      unbinder.unbind();
      view1.performClick();
      assertEquals(1, target.clicks);
    });
  }

  static final class MultipleBindings {
    int clicks = 0;

    @OnClick(1) void click1() {
      clicks++;
    }

    @OnClick(1) void clicks2() {
      clicks++;
    }
  }

  @Test public void multipleBindings() {
    assumeFalse("Not implemented", BuildConfig.FLAVOR.equals("reflect")); // TODO

    View tree = ViewTree.create(1);
    View view1 = tree.findViewById(1);

    MultipleBindings target = new MultipleBindings();
    Unbinder unbinder = ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertEquals(2, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      unbinder.unbind();
      view1.performClick();
      assertEquals(2, target.clicks);
    });
  }

  static final class Visibilities {
    int clicks = 0;

    @OnClick(1) public void publicClick() {
      clicks++;
    }

    @OnClick(2) void packageClick() {
      clicks++;
    }

    @OnClick(3) protected void protectedClick() {
      clicks++;
    }
  }

  @Test public void visibilities() {
    View tree = ViewTree.create(1, 2, 3);
    View view1 = tree.findViewById(1);
    View view2 = tree.findViewById(2);
    View view3 = tree.findViewById(3);

    Visibilities target = new Visibilities();
    ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertEquals(1, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      view2.performClick();
      assertEquals(2, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      view3.performClick();
      assertEquals(3, target.clicks);
    });
  }

  static final class MultipleIds {
    int clicks = 0;

    @OnClick({1, 2}) void click() {
      clicks++;
    }
  }

  @Test public void multipleIds() {
    View tree = ViewTree.create(1, 2);
    View view1 = tree.findViewById(1);
    View view2 = tree.findViewById(2);

    MultipleIds target = new MultipleIds();
    Unbinder unbinder = ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertEquals(1, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      view2.performClick();
      assertEquals(2, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      unbinder.unbind();
      view1.performClick();
      view2.performClick();
      assertEquals(2, target.clicks);
    });
  }

  static final class OptionalId {
    int clicks = 0;

    @Optional @OnClick(1) public void click() {
      clicks++;
    }
  }

  @Test public void optionalIdPresent() {
    View tree = ViewTree.create(1);
    View view1 = tree.findViewById(1);

    OptionalId target = new OptionalId();
    Unbinder unbinder = ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertEquals(1, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      unbinder.unbind();
      view1.performClick();
      assertEquals(1, target.clicks);
    });
  }

  @Test public void optionalIdAbsent() {
    View tree = ViewTree.create(2);
    View view2 = tree.findViewById(2);

    OptionalId target = new OptionalId();
    Unbinder unbinder = ButterKnife.bind(target, tree);
    assertEquals(0, target.clicks);

    instrumentation.runOnMainSync(() -> {
      view2.performClick();
      assertEquals(0, target.clicks);
    });

    instrumentation.runOnMainSync(() -> {
      unbinder.unbind();
      view2.performClick();
      assertEquals(0, target.clicks);
    });
  }

  static final class ArgumentCast {
    interface MyInterface {}

    View last;

    @OnClick(1) void clickView(View view) {
      last = view;
    }

    @OnClick(2) void clickTextView(TextView view) {
      last = view;
    }

    @OnClick(3) void clickButton(Button view) {
      last = view;
    }

    @OnClick(4) void clickMyInterface(MyInterface view) {
      last = (View) view;
    }
  }

  @Test public void argumentCast() {
    class MyView extends Button implements ArgumentCast.MyInterface {
      MyView(Context context) {
        super(context);
      }
    }

    View view1 = new MyView(InstrumentationRegistry.getContext());
    view1.setId(1);
    View view2 = new MyView(InstrumentationRegistry.getContext());
    view2.setId(2);
    View view3 = new MyView(InstrumentationRegistry.getContext());
    view3.setId(3);
    View view4 = new MyView(InstrumentationRegistry.getContext());
    view4.setId(4);
    ViewGroup tree = new FrameLayout(InstrumentationRegistry.getContext());
    tree.addView(view1);
    tree.addView(view2);
    tree.addView(view3);
    tree.addView(view4);

    ArgumentCast target = new ArgumentCast();
    ButterKnife.bind(target, tree);

    instrumentation.runOnMainSync(() -> {
      view1.performClick();
      assertSame(view1, target.last);
    });

    instrumentation.runOnMainSync(() -> {
      view2.performClick();
      assertSame(view2, target.last);
    });

    instrumentation.runOnMainSync(() -> {
      view3.performClick();
      assertSame(view3, target.last);
    });

    instrumentation.runOnMainSync(() -> {
      view4.performClick();
      assertSame(view4, target.last);
    });
  }
}