Sunday, March 31, 2013

Nulls and varargs

I needed to work out how you can invoke methods that have varargs when you don't actually have anything to send for that parameter. Below are the lessons I have learned and below that is the JUnit test that proves it. I am using JDK 7.

  1. When a method has a varargs argument, you can completely ignore it and not put anything at all in that place.
  2. A varargs parameter always has a type, and you can send a single value of that type - or an array of that type.
  3. The array can be empty - but your code should be able to deal with that.
  4. The parameter can be null, although Eclipse wants you cast the null - and your code must be prepared to deal with nulls (or you will throw a NullPointerException).
  5. When using mockit.Deencapsulation to invoke a method that accepts varargs, you cannot ignore the parameter; it has to have something there or JMockit will throw an IllegalArgumentException.
  6. mockit.Deencapsulation throws an IllegalArgumentException if you send null (cast or not) in place of the varargs because null isn't enough information for JMockit to work out what type of argument it is.
  7. You can tell mockit.Deencapsulation to send null in place of the varargs by using a X[].class as the parameter (where X is the type expected by varargs) - and your code must be prepared to deal with nulls (or you will throw a NullPointerException). See http://bit.ly/ZMYZuR.

Here is the test.

import static org.junit.Assert.assertNotNull;

import java.nio.file.Paths;

import mockit.Deencapsulation;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Test var args. What calling a method with varargs, you can totally leave out
 * the vararg parameter, but you cannot use null - unless you cast it. But you
 * also can't use a null (even a casted one) when you are using JMockit
 * {@link Deencapsulation#invoke(Class, String, Object...)}.
 *
 * @author RobertMarkBram
 */
@RunWith(JUnit4.class)
public final class TestVarArgs {

  /**
   * A method with var args.
   *
   * @param first
   *           first path
   * @param more
   *           the rest - may be null
   */
  public void readFiles(final String first, final String... more) {
    if (more == null || more.length == 0 || more[0] == null) {
      assertNotNull(Paths.get(first).toString());
    } else {
      assertNotNull(Paths.get(first, more).toString());
    }
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg should
   * work. Completely ignoring the vararg.
   */
  @Test
  public void callMethodWithOneArg() {
    readFiles("C:/Temp");
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and an
   * empty string should work.
   */
  @Test
  public void callMethodWithOneArgAndEmptyString() {
    readFiles("C:/Temp", "");
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and an
   * empty array should work.
   */
  @Test
  public void callMethodWithOneArgAndEmptyArray() {
    String[] empty = {};
    readFiles("C:/Temp", empty);
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and an
   * array with a blank string should work.
   */
  @Test
  public void callMethodWithOneArgAndArrayWithEmptyString() {
    String[] empty = { "" };
    readFiles("C:/Temp", empty);
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and a null
   * works, but you have to do a null check on the varargs in case it is null.
   * Plus, Eclipse gives you a warning for not casting null as vararg.
   */
  @Test
  @SuppressWarnings("all")
  public void callMethodWithOneArgAndNull() {
    readFiles("C:/Temp", null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and a null
   * cast to the expected type works, but you have to do a null check on the
   * varargs in case it is null.
   */
  @Test
  public void callMethodWithOneArgAndStringCastNull() {
    readFiles("C:/Temp", (String) null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} with just one arg and a null
   * cast to the expected type works, but you have to do a null check on the
   * varargs in case it is null.
   */
  @Test
  public void callMethodWithOneArgAndCastNull() {
    readFiles("C:/Temp", (String[]) null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg fails with an {@link IllegalArgumentException} because JMockit
   * requires a valid varargs argument, i.e., not null.
   *
   * @see http://bit.ly/ZMYZuR
   */
  @Test(expected = IllegalArgumentException.class)
  public void callMethodWithOneArgJmockit() {
    Deencapsulation.invoke(this, "readFiles", "C:/Temp");
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg fails with an {@link IllegalArgumentException} because JMockit
   * requires a valid varargs argument, i.e., not a single element of the
   * array.
   *
   * @see http://bit.ly/ZMYZuR
   */
  @Test(expected = IllegalArgumentException.class)
  public void callMethodWithOneArgAndEmptyStringJmockit() {
    Deencapsulation.invoke(this, "readFiles", "");
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and an empty array should work.
   */
  @Test
  public void callMethodWithOneArgAndEmptyArrayJmockit() {
    String[] empty = {};
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", empty);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and an array with a blank string should work.
   */
  @Test
  public void callMethodWithOneArgAndArrayWithEmptyStringJmockit() {
    String[] empty = { "" };
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", empty);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and a null fails with an {@link IllegalArgumentException} because
   * JMockit requires a valid varargs argument, i.e., not null.
   *
   * @see http://bit.ly/ZMYZuR
   */
  @Test(expected = IllegalArgumentException.class)
  public void callMethodWithOneArgAndNullJmockit() {
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and a null cast to the expected type fails with an
   * {@link IllegalArgumentException} because JMockit requires a valid varargs
   * argument, i.e., not null.
   *
   * @see http://bit.ly/ZMYZuR
   */
  @Test(expected = IllegalArgumentException.class)
  public void callMethodWithOneArgAndCastNullJmockit() {
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", (String[]) null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and a null cast to the expected type fails with an
   * {@link IllegalArgumentException} because JMockit requires a valid varargs
   * argument, i.e., not null.
   *
   * @see http://bit.ly/ZMYZuR
   */
  @Test(expected = IllegalArgumentException.class)
  public void callMethodWithOneArgAndStringCastNullJmockit() {
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", (String) null);
  }

  /**
   * Calling {@link #readFiles(String, String...)} via JMockit with just one
   * arg and a null cast to the expected type fails with an
   * {@link IllegalArgumentException} because JMockit requires a valid varargs
   * argument, i.e., not null.
   *
   * @see http://bit.ly/ZMYZuR
   */
  public void callMethodWithOneArgAndNullArrayJmockit() {
    Deencapsulation.invoke(this, "readFiles", "C:/Temp", String[].class);
  }
}

Further reading.