项目场景:
最近在搞单元测试,在逐步摸索中,记录一些小坑、小知识点。
本文为 mock出来的对象的属性值与get方法取到的属性值不一致的问题。
问题描述
例如:如下场景,needCheck为我mock的对象
NeedCheck needCheck = Mockito.mock(NeedCheck.class);
发现最终的结果不是我断言的结果,就进行debug调试,如下,很奇怪为什么能进入这个if中
最开始我没有注意到
Byte chklevel = needCheck.getChklevel();
这一行代码。就一直很奇怪:needCheck对象的maxchklevel为null,needCheck.getMaxchklevel()也应该是null,前面chklevel 为byte(0),这俩怎么可能相等呢???
后来发给同事看,同事发现
Byte chklevel = needCheck.getChklevel();
这一行代码,chklevel 为byte(0),但是needCheck对象的chklevel为null。发现这个之后,我就去看了一下needCheck.getMaxchklevel()的值
果然也是byte(0),这样它们相等就是对的,果然代码是不会骗人的😂,但是问题来了,为什么属性为null但是get取到的值却不是null呢?
原因分析:
很自然的想到了,mock出来的对象被处理了默认值,查看这个Mockito.mock()方法
@CheckReturnValue
public static <T> T mock(Class<T> classToMock) {
return mock(classToMock, withSettings());
}
发现除了我们传过来要mock的类以外,还有一个参数withSettings(),继续查看withSettings()
@CheckReturnValue
public static MockSettings withSettings() {
return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
}
继续查看defaultAnswer,发现其实RETURNS_DEFAULTS是一个Answer实现
@Override
public MockSettings defaultAnswer(Answer defaultAnswer) {
this.defaultAnswer = defaultAnswer;
if (defaultAnswer == null) {
throw defaultAnswerDoesNotAcceptNullParameter();
}
return this;
}
查看Answer类,注释表明Answer用于处理mock对象的返回值。继续查看RETURNS_DEFAULTS,发现它是在Mockito类中定义的,等于Answers.RETURNS_DEFAULTS;
/**
* The default <code>Answer</code> of every mock <b>if</b> the mock was not stubbed.
*
* Typically it just returns some empty value.
* <p>
* {@link Answer} can be used to define the return values of unstubbed invocations.
* <p>
* This implementation first tries the global configuration and if there is no global configuration then
* it will use a default answer that returns zeros, empty collections, nulls, etc.
*/
public static final Answer<Object> RETURNS_DEFAULTS = Answers.RETURNS_DEFAULTS;
继续找Answers.RETURNS_DEFAULTS,发现Answers是一个枚举类
/**
* Enumeration of pre-configured mock answers
* <p>
* You can use it to pass extra parameters to @Mock annotation, see more info here: {@link Mock}
* <p>
* Example:
* <pre class="code"><code class="java">
* @Mock(answer = RETURNS_DEEP_STUBS) UserProvider userProvider;
* </code></pre>
* <b>This is not the full list</b> of Answers available in Mockito. Some interesting answers can be found in org.mockito.stubbing.answers package.
*/
public enum Answers implements Answer<Object> {
/**
* The default configured answer of every mock.
*
* <p>Please see the {@link org.mockito.Mockito#RETURNS_DEFAULTS} documentation for more details.</p>
*
* @see org.mockito.Mockito#RETURNS_DEFAULTS
*/
RETURNS_DEFAULTS(new GloballyConfiguredAnswer()),
/**
* An answer that returns smart-nulls.
*
* <p>Please see the {@link org.mockito.Mockito#RETURNS_SMART_NULLS} documentation for more details.</p>
*
* @see org.mockito.Mockito#RETURNS_SMART_NULLS
*/
RETURNS_SMART_NULLS(new ReturnsSmartNulls()),
/**
* An answer that returns <strong>mocks</strong> (not stubs).
*
* <p>Please see the {@link org.mockito.Mockito#RETURNS_MOCKS} documentation for more details.</p>
*
* @see org.mockito.Mockito#RETURNS_MOCKS
*/
RETURNS_MOCKS(new ReturnsMocks()),
/**
* An answer that returns <strong>deep stubs</strong> (not mocks).
*
* <p>Please see the {@link org.mockito.Mockito#RETURNS_DEEP_STUBS} documentation for more details.</p>
*
* @see org.mockito.Mockito#RETURNS_DEEP_STUBS
*/
RETURNS_DEEP_STUBS(new ReturnsDeepStubs()),
/**
* An answer that calls the real methods (used for partial mocks).
*
* <p>Please see the {@link org.mockito.Mockito#CALLS_REAL_METHODS} documentation for more details.</p>
*
* @see org.mockito.Mockito#CALLS_REAL_METHODS
*/
CALLS_REAL_METHODS(new CallsRealMethods()),
/**
* An answer that tries to return itself. This is useful for mocking {@code Builders}.
*
* <p>Please see the {@link org.mockito.Mockito#RETURNS_SELF} documentation for more details.</p>
*
* @see org.mockito.Mockito#RETURNS_SELF
*/
RETURNS_SELF(new TriesToReturnSelf());
private final Answer<Object> implementation;
Answers(Answer<Object> implementation) {
this.implementation = implementation;
}
/**
* @deprecated as of 2.1.0 Use the enum-constant directly, instead of this getter. This method will be removed in a future release<br>
* E.g. instead of <code>Answers.CALLS_REAL_METHODS.get()</code> use <code>Answers.CALLS_REAL_METHODS</code> .
*/
@Deprecated
public Answer<Object> get() {
return this;
}
public Object answer(InvocationOnMock invocation) throws Throwable {
return implementation.answer(invocation);
}
}
一共有6个对象,暂时先放过。查看GloballyConfiguredAnswer()
org.mockito.internal.stubbing.defaultanswers.GloballyConfiguredAnswer
public Object answer(InvocationOnMock invocation) throws Throwable {
return new GlobalConfiguration().getDefaultAnswer().answer(invocation);
}
org.mockito.internal.configuration.GlobalConfiguration
public Answer<Object> getDefaultAnswer() {
return GLOBAL_CONFIGURATION.get().getDefaultAnswer();
}
org.mockito.stubbing.Answer.IMockitoConfiguration
Answer<Object> getDefaultAnswer();
找到它的实现类org.mockito.configuration.DefaultMockitoConfiguration
public Answer<Object> getDefaultAnswer() {
return new ReturnsEmptyValues();
}
查看ReturnsEmptyValues,发现终于找到头了🤣
public class ReturnsEmptyValues implements Answer<Object>, Serializable {
private static final long serialVersionUID = 1998191268711234347L;
/* (non-Javadoc)
* @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock)
*/
public Object answer(InvocationOnMock invocation) {
if (isToStringMethod(invocation.getMethod())) {
Object mock = invocation.getMock();
MockName name = MockUtil.getMockName(mock);
if (name.isDefault()) {
return "Mock for "
+ MockUtil.getMockSettings(mock).getTypeToMock().getSimpleName()
+ ", hashCode: "
+ mock.hashCode();
} else {
return name.toString();
}
} else if (isCompareToMethod(invocation.getMethod())) {
// see issue 184.
// mocks by default should return 0 if references are the same, otherwise some other
// value because they are not the same. Hence we return 1 (anything but 0 is good).
// Only for compareTo() method by the Comparable interface
return invocation.getMock() == invocation.getArgument(0) ? 0 : 1;
}
Class<?> returnType = invocation.getMethod().getReturnType();
return returnValueFor(returnType);
}
Object returnValueFor(Class<?> type) {
if (Primitives.isPrimitiveOrWrapper(type)) {
return Primitives.defaultValue(type);
// new instances are used instead of Collections.emptyList(), etc.
// to avoid UnsupportedOperationException if code under test modifies returned
// collection
} else if (type == Iterable.class) {
return new ArrayList<Object>(0);
} else if (type == Collection.class) {
return new LinkedList<Object>();
} else if (type == Set.class) {
return new HashSet<Object>();
} else if (type == HashSet.class) {
return new HashSet<Object>();
} else if (type == SortedSet.class) {
return new TreeSet<Object>();
} else if (type == TreeSet.class) {
return new TreeSet<Object>();
} else if (type == LinkedHashSet.class) {
return new LinkedHashSet<Object>();
} else if (type == List.class) {
return new LinkedList<Object>();
} else if (type == LinkedList.class) {
return new LinkedList<Object>();
} else if (type == ArrayList.class) {
return new ArrayList<Object>();
} else if (type == Map.class) {
return new HashMap<Object, Object>();
} else if (type == HashMap.class) {
return new HashMap<Object, Object>();
} else if (type == SortedMap.class) {
return new TreeMap<Object, Object>();
} else if (type == TreeMap.class) {
return new TreeMap<Object, Object>();
} else if (type == LinkedHashMap.class) {
return new LinkedHashMap<Object, Object>();
} else if ("java.util.Optional".equals(type.getName())) {
return JavaEightUtil.emptyOptional();
} else if ("java.util.OptionalDouble".equals(type.getName())) {
return JavaEightUtil.emptyOptionalDouble();
} else if ("java.util.OptionalInt".equals(type.getName())) {
return JavaEightUtil.emptyOptionalInt();
} else if ("java.util.OptionalLong".equals(type.getName())) {
return JavaEightUtil.emptyOptionalLong();
} else if ("java.util.stream.Stream".equals(type.getName())) {
return JavaEightUtil.emptyStream();
} else if ("java.util.stream.DoubleStream".equals(type.getName())) {
return JavaEightUtil.emptyDoubleStream();
} else if ("java.util.stream.IntStream".equals(type.getName())) {
return JavaEightUtil.emptyIntStream();
} else if ("java.util.stream.LongStream".equals(type.getName())) {
return JavaEightUtil.emptyLongStream();
} else if ("java.time.Duration".equals(type.getName())) {
return JavaEightUtil.emptyDuration();
} else if ("java.time.Period".equals(type.getName())) {
return JavaEightUtil.emptyPeriod();
}
// Let's not care about the rest of collections.
return null;
}
}
两个if条件
isToStringMethod应该是处理toString的,isCompareToMethod应该是处理compareTo的,get方法进不去,本次不关注,有兴趣的可以了解一下
咱们就看returnValueFor,根据对mock对象使用的方法的返回类型,进行不同处理。上面贴出来的代码,都是处理集合类型的(set、list、map),返回一个对应的空集合。我们看一下第一个if条件
if (Primitives.isPrimitiveOrWrapper(type)) {
return Primitives.defaultValue(type);
// new instances are used instead of Collections.emptyList(), etc.
// to avoid UnsupportedOperationException if code under test modifies returned
// collection
}
接着看Primitives.isPrimitiveOrWrapper(type),PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES是一个map,containsKey进行判断
public static boolean isPrimitiveOrWrapper(Class<?> type) {
return PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.containsKey(type);
}
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES里都有什么呢?
static {
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Boolean.class, false);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Character.class, '\u0000');
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Byte.class, (byte) 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Short.class, (short) 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Integer.class, 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Long.class, 0L);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Float.class, 0F);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(Double.class, 0D);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(boolean.class, false);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(char.class, '\u0000');
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(byte.class, (byte) 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(short.class, (short) 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(int.class, 0);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(long.class, 0L);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(float.class, 0F);
PRIMITIVE_OR_WRAPPER_DEFAULT_VALUES.put(double.class, 0D);
}
这样就清楚了,根据返回类型,返回各种默认值。
Byte会返回(byte) 0,所以出现属性值为null,get值不为null的情况。
另外可以看到String类型是没有处理的,所以走到最后,会返回null。
接着我们看一下Answers枚举类的六个对象,都是什么用途,怎么用的
来到org.mockito.Answers 居然又让我们去看org.mockito.Mockito😂
Answers | 解释(机器翻译) |
---|---|
RETURNS_DEFAULTS | 此实现首先尝试全局配置,如果没有全局配置,则将使用返回零、空集合、空值等的默认答案。 |
RETURNS_SMART_NULLS | 在处理遗留代码时,此实现可能会有所帮助。未创建的方法通常返回null。如果您的代码使用未经subbed调用返回的对象,则会得到NullPointerException。这个实现返回SmartNull,而不是null。 |
RETURNS_MOCKS | 在处理遗留代码时,此实现可能会有所帮助。ReturnsMocks首先尝试返回普通值(零、空集合、空字符串等),然后尝试返回mock。如果无法模拟返回类型(例如为final),则返回纯null 。 |
RETURNS_DEEP_STUBS | 大概意思就是适用于链式调用。person.getAddress(anyString()).getStreet().getName() |
CALLS_REAL_METHODS | 当使用此实现时,未建模的方法将委托给真正的实现。这是一种创建默认情况下调用真实方法的部分模拟对象的方法。 |
RETURNS_SELF | 允许生成器模拟在调用返回与类或父类相同类型的方法时返回自身。 |
使用方法:
public static <T> T mock(Class<T> classToMock, Answer defaultAnswer) {
return mock(classToMock, withSettings().defaultAnswer(defaultAnswer));
}
NeedCheck needCheck = Mockito.mock(NeedCheck.class, Mockito.CALLS_REAL_METHODS);
PS:也可以自己实现一个Answer ,按如上方式进行mock。
注:使用的是springboot2.4.8
junit以及mockito为spring-boot-starter-test引入的,对应的org.junit.jupiter版本为5.7.2,org.mockito版本为3.6.28
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
总结:
本文只是研究了一下为什么属性值和get值取到的结果不一致。mock对象还是使用默认的就好,暂时没有什么使用场景,包括其他几种Answers。不过能知道可以进行处理,也是不错的收获。