All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatest.PrivateMethodTester.scala Maven / Gradle / Ivy

There is a newer version: 2.0.M6-SNAP27
Show newest version
/*
 * Copyright 2001-2008 Artima, 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 org.scalatest

import java.lang.reflect.{InvocationTargetException, Method, Modifier}

/**
 * Trait that facilitates the testing of private methods.
 *
 * To test a private method, mix in trait PrivateMethodTester and
 * create a PrivateMethod object, like this: 
 *
 * 
 * val decorateToStringValue = PrivateMethod[String]('decorateToStringValue)
 * 
* *

* The type parameter on PrivateMethod, in this case String, is the result type of the private method * you wish to invoke. The symbol passed to the PrivateMethod.apply factory method, in this * case 'decorateToStringValue, is the name of the private method to invoke. To test * the private method, use the invokePrivate operator, like this: *

* *
 * targetObject invokePrivate decorateToStringValue(1)
 * 
* *

* Here, targetObject is a variable or singleton object name referring to the object whose * private method you want to test. You pass the arguments to the private method in the parentheses after * the PrivateMethod object. * The result type of an invokePrivate operation will be the type parameter of the PrivateMethod * object, thus you need not cast the result to use it. In other words, after creating a PrivateMethod object, the * syntax to invoke the private method * looks like a regular method invocation, but with the dot (.) replaced by invokePrivate. * The private method is invoked dynamically via reflection, so if you have a typo in the method name symbol, specify the wrong result type, * or pass invalid parameters, the invokePrivate operation will compile, but throw an exception at runtime. *

* *

* One limitation to be aware of is that you can't use PrivateMethodTester to test a private method * declared in a trait, because the class the trait gets mixed into will not declare that private method. Only the * class generated to hold method implementations for the trait will have that private method. If you want to * test a private method declared in a trait, and that method does not use any state of that trait, you can move * the private method to a companion object for the trait and test it using PrivateMethodTester that * way. If the private trait method you want to test uses the trait's state, your best options are to test it * indirectly via a non-private trait method that calls the private method, or make the private method package access * and test it directly via regular static method invocations. *

* * @author Bill Venners */ trait PrivateMethodTester { /** * Represent a private method, whose apply method returns an Invocation object that * records the name of the private method to invoke, and any arguments to pass to it when invoked. * The type parameter, T, is the return type of the private method. * * @param methodName a Symbol representing the name of the private method to invoke * @throws NullPointerException if methodName is null */ final class PrivateMethod[T] private (methodName: Symbol) { if (methodName == null) throw new NullPointerException("methodName was null") /** * Apply arguments to a private method. This method returns an Invocation * object, ready to be passed to an invokePrivate method call. * The type parameter, T, is the return type of the private method. * * @param args zero to many arguments to pass to the private method when invoked * @return an Invocation object that can be passed to invokePrivate to invoke * the private method */ def apply(args: Any*) = new Invocation[T](methodName, args: _*) } /** * Contains a factory method for instantiating PrivateMethod objects. */ object PrivateMethod { /** * Construct a new PrivateMethod object with passed methodName symbol. * The type parameter, T, is the return type of the private method. * * @param methodName a Symbol representing the name of the private method to invoke * @throws NullPointerException if methodName is null */ def apply[T](methodName: Symbol) = new PrivateMethod[T](methodName) } /** * Class whose instances represent an invocation of a private method. Instances of this * class contain the name of the private method (methodName) and the arguments * to pass to it during the invocation (args). * The type parameter, T, is the return type of the private method. * * @param methodName a Symbol representing the name of the private method to invoke * @param args zero to many arguments to pass to the private method when invoked * @throws NullPointerException if methodName is null */ final class Invocation[T](val methodName: Symbol, val args: Any*) { if (methodName == null) throw new NullPointerException } /** * Class used via an implicit conversion to enable private methods to be tested. */ final class Invoker(target: AnyRef) { if (target == null) throw new NullPointerException /** * Invoke a private method. This method will attempt to invoke via reflection a private method. * The name of the method to invoke is contained in the methodName field of the passed Invocation. * The arguments to pass are contained in the args field. The object on which to invoke the private * method is the target object passed to this Invoker's primary constructor. * The type parameter, T, is the return type of the private method. * * @param invocation the Invocation object containing the method name symbol and args of the invocation. * @return the value returned by the invoked private method * @throws IllegalArgumentException if the target object does not have a method of the name, with argument types * compatible with the objects in the passed args array, specified in the passed Invocation object. */ def invokePrivate[T](invocation: Invocation[T]): T = { import invocation._ // If 'getMessage passed as methodName, methodNameToInvoke would be "getMessage" val methodNameToInvoke = methodName.toString.substring(1) def isMethodToInvoke(m: Method) = { val isInstanceMethod = !Modifier.isStatic(m.getModifiers()) val simpleName = m.getName val paramTypes = m.getParameterTypes val isPrivate = Modifier.isPrivate(m.getModifiers()) // The AnyVals must go in as Java wrapper types. But the type is still Any, so this needs to be converted // to AnyRef for the compiler to be happy. Implicit conversions are ambiguous, and really all that's needed // is a type cast, so I use isInstanceOf. def argsHaveValidTypes: Boolean = { // First, the arrays must have the same length: if (args.length == paramTypes.length) { val zipped = args.toList zip paramTypes.toList // If arg.asInstanceOf[AnyRef] has class java.lang.Integer, this needs to match the paramType Class instance for int def argMatchesParamType(arg: Any, paramType: Class[_]) = { val anyRefArg = arg.asInstanceOf[AnyRef] paramType match { case java.lang.Long.TYPE => anyRefArg.getClass == classOf[java.lang.Long] case java.lang.Integer.TYPE => anyRefArg.getClass == classOf[java.lang.Integer] case java.lang.Short.TYPE => anyRefArg.getClass == classOf[java.lang.Short] case java.lang.Byte.TYPE => anyRefArg.getClass == classOf[java.lang.Byte] case java.lang.Character.TYPE => anyRefArg.getClass == classOf[java.lang.Character] case java.lang.Double.TYPE => anyRefArg.getClass == classOf[java.lang.Double] case java.lang.Float.TYPE => anyRefArg.getClass == classOf[java.lang.Float] case java.lang.Boolean.TYPE => anyRefArg.getClass == classOf[java.lang.Boolean] case _ => paramType.isAssignableFrom(anyRefArg.getClass) } } // The args classes need only be assignable to the parameter type. So therefore the parameter type // must be assignable *from* the corresponding arg class type. val invalidArgs = for ((arg, paramType) <- zipped if !argMatchesParamType(arg, paramType)) yield arg invalidArgs.length == 0 } else false } /* The rules may be that private mehods in standalone objects currently get name mangled and made public, perhaps because there are two versions of each private method, one in the actual singleton and one int the class that also has static methods, and one calls the other. So if this is true, then I may change this to say if simpleName matches exactly and its private, or if ends with simpleName prepended by two dollar signs, then let it be public, but look for whatever the Scala compiler puts in there to mark it as private at the Scala source level. // org$scalatest$FailureMessages$$decorateToStringValue // 0 org$scalatest$FailureMessages$$decorateToStringValue [java] 1 true [java] 2 false [java] false [java] false [java] ^&^&^&^&^&^& invalidArgs.length is: 0 [java] 5 true println("0 "+ simpleName) println("1 "+ isInstanceMethod) println("2 "+ isPrivate) println("3 "+ simpleName == methodNameToInvoke) println("4 "+ candidateResultType == resultType) println("5 "+ argsHaveValidTypes) This ugliness. I'll ignore the result type for now. Sheesh. Investigate that one. And I'll have to ignore private too for now, because in the bytecodes it isn't even private. And I'll also allow methods that end with $$ if the simpleName doesn't match */ isInstanceMethod && (simpleName == methodNameToInvoke || simpleName.endsWith("$$"+ methodNameToInvoke)) && argsHaveValidTypes } // Store in an array, because may have both isEmpty and empty, in which case I // will throw an exception. val methodArray = for (m <- target.getClass.getDeclaredMethods; if isMethodToInvoke(m)) yield m if (methodArray.length == 0) throw new IllegalArgumentException("Can't find a private method named: " + methodNameToInvoke) else if (methodArray.length > 1) throw new IllegalArgumentException("Found two methods") else { val anyRefArgs = // Need to box these myself, because that's invoke is expecting an Array[Object], which maps to an Array[AnyRef] for (arg <- args) yield arg match { case anyRef: AnyRef => anyRef case any: Any => any.asInstanceOf[AnyRef] // Can't use AnyVal in 2.8 } val privateMethodToInvoke = methodArray(0) privateMethodToInvoke.setAccessible(true) try { privateMethodToInvoke.invoke(target, anyRefArgs.toArray: _*).asInstanceOf[T] } catch { case e: InvocationTargetException => val cause = e.getCause if (cause != null) throw cause else throw e } } } } /** * Implicit conversion from AnyRef to Invoker, used to enable * assertions testing of private methods. * * @param target the target object on which to invoke a private method. * @throws NullPointerException if target is null. */ implicit def anyRefToInvoker(target: AnyRef): Invoker = new Invoker(target) } /** * Companion object that facilitates the importing of PrivateMethodTester members as * an alternative to mixing it in. One use case is to import PrivateMethodTester members so you can use * them in the Scala interpreter: * *
 * $scala -classpath scalatest.jar
 * Welcome to Scala version 2.7.5.final (Java HotSpot(TM) Client VM, Java 1.5.0_16).
 * Type in expressions to have them evaluated.
 * Type :help for more information.
 *  
 * scala> import org.scalatest.PrivateMethodTester._                 
 * import org.scalatest.PrivateMethodTester._
 *  
 * scala> class Example {
 *      |   private def addSesame(prefix: String) = prefix + " sesame"
 *      | }
 * defined class Example
 *  
 * scala> val example = new Example
 * example: Example = Example@d8b6fe
 *  
 * scala> val addSesame = PrivateMethod[String]('addSesame)           
 * addSesame: org.scalatest.PrivateMethodTester.PrivateMethod[String] = org.scalatest.PrivateMethodTester$PrivateMethod@5cdf95
 *  
 * scala> example invokePrivate addSesame("open")                     
 * res0: String = open sesame
 * 
 *
 * @author Bill Venners
 */
object PrivateMethodTester extends PrivateMethodTester





© 2015 - 2024 Weber Informatics LLC | Privacy Policy