org.joda.beans.test.JodaBeanTests Maven / Gradle / Ivy
/*
* Copyright 2001-present Stephen Colebourne
*
* 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.joda.beans.test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.ImmutableBean;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaBean;
import org.joda.beans.MetaProperty;
import org.joda.beans.impl.StandaloneMetaProperty;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaProperty;
/**
* A utility class to assist with testing beans.
*
* Test coverage statistics can be heavily skewed by getters, setters and generated code.
* This class provides a solution, allowing bean test coverage to be artificially increased.
* Always remember that the goal of artificially increasing coverage is so that you can
* see what you really need to test, not to avoid writing tests altogether.
*
* @author Stephen Colebourne
*/
public final class JodaBeanTests {
/**
* This constant can be used to pass to increase test coverage.
* This is used by some {@link MetaBean} methods in generated classes.
*/
public static final String TEST_COVERAGE_PROPERTY = "!ConstantUsedForTestCoveragePurposes!";
/**
* This constant can be used to pass to increase test coverage.
* This is used by some {@link BeanBuilder} set methods in generated classes.
*/
public static final String TEST_COVERAGE_STRING = "!ConstantUsedForTestCoveragePurposes!";
//-------------------------------------------------------------------------
/**
* Test a mutable bean for the primary purpose of increasing test coverage.
*
* @param bean the bean to test
*/
public static void coverMutableBean(Bean bean) {
assertNotNull(bean, "coverImmutableBean() called with null bean");
assertFalse(bean instanceof ImmutableBean);
assertNotSame(JodaBeanUtils.clone(bean), bean);
coverBean(bean);
}
/**
* Test an immutable bean for the primary purpose of increasing test coverage.
*
* @param bean the bean to test
*/
public static void coverImmutableBean(ImmutableBean bean) {
assertNotNull(bean, "coverImmutableBean() called with null bean");
assertSame(JodaBeanUtils.clone(bean), bean);
coverBean(bean);
}
/**
* Test a bean equals method for the primary purpose of increasing test coverage.
*
* The two beans passed in should contain a different value for each property.
* The method creates a cross-product to ensure test coverage of equals.
*
* @param bean1 the first bean to test
* @param bean2 the second bean to test
*/
@SuppressWarnings("unlikely-arg-type")
public static void coverBeanEquals(Bean bean1, Bean bean2) {
assertNotNull(bean1, "coverBeanEquals() called with null bean");
assertNotNull(bean2, "coverBeanEquals() called with null bean");
assertFalse(bean1.equals(null));
assertFalse(bean1.equals("NonBean"));
assertTrue(bean1.equals(bean1));
assertTrue(bean2.equals(bean2));
ignoreThrows(() -> assertEquals(bean1, JodaBeanUtils.cloneAlways(bean1)));
ignoreThrows(() -> assertEquals(bean2, JodaBeanUtils.cloneAlways(bean2)));
assertTrue(bean1.hashCode() == bean1.hashCode());
assertTrue(bean2.hashCode() == bean2.hashCode());
if (bean1.equals(bean2) || bean1.getClass() != bean2.getClass()) {
return;
}
MetaBean metaBean = bean1.metaBean();
List> buildableProps = metaBean.metaPropertyMap().values().stream()
.filter(mp -> mp.style().isBuildable())
.collect(Collectors.toList());
Set builtBeansSet = new HashSet<>();
builtBeansSet.add(bean1);
builtBeansSet.add(bean2);
for (int i = 0; i < buildableProps.size(); i++) {
for (int j = 0; j < 2; j++) {
try {
BeanBuilder extends Bean> bld = metaBean.builder();
for (int k = 0; k < buildableProps.size(); k++) {
MetaProperty> mp = buildableProps.get(k);
if (j == 0) {
bld.set(mp, mp.get(k < i ? bean1 : bean2));
} else {
bld.set(mp, mp.get(i <= k ? bean1 : bean2));
}
}
builtBeansSet.add(bld.build());
} catch (RuntimeException ex) {
// ignore
}
}
}
List builtBeansList = new ArrayList<>(builtBeansSet);
for (int i = 0; i < builtBeansList.size() - 1; i++) {
for (int j = i + 1; j < builtBeansList.size(); j++) {
builtBeansList.get(i).equals(builtBeansList.get(j));
}
}
}
// provide test coverage to all beans
private static void coverBean(Bean bean) {
coverProperties(bean);
coverNonProperties(bean);
coverEquals(bean);
}
// cover parts of a bean that are property-based
private static void coverProperties(Bean bean) {
MetaBean metaBean = bean.metaBean();
Map> metaPropMap = metaBean.metaPropertyMap();
assertNotNull(metaPropMap);
assertEquals(metaBean.metaPropertyCount(), metaPropMap.size());
for (MetaProperty> mp : metaBean.metaPropertyIterable()) {
assertTrue(metaBean.metaPropertyExists(mp.name()));
assertEquals(metaBean.metaProperty(mp.name()), mp);
// Ensure we don't use interned value
assertEquals(metaBean.metaProperty(new String(mp.name())), mp);
assertEquals(metaPropMap.values().contains(mp), true);
assertEquals(metaPropMap.keySet().contains(mp.name()), true);
if (mp.style().isReadable()) {
ignoreThrows(() -> mp.get(bean));
} else {
assertThrows(() -> mp.get(bean), UnsupportedOperationException.class);
}
if (mp.style().isWritable()) {
ignoreThrows(() -> mp.set(bean, ""));
} else {
assertThrows(() -> mp.set(bean, ""), UnsupportedOperationException.class);
}
if (mp.style().isBuildable()) {
ignoreThrows(() -> metaBean.builder().get(mp));
ignoreThrows(() -> metaBean.builder().get(mp.name()));
for (Object setValue : sampleValues(mp)) {
ignoreThrows(() -> metaBean.builder().set(mp, setValue));
}
for (Object setValue : sampleValues(mp)) {
ignoreThrows(() -> metaBean.builder().set(mp.name(), setValue));
}
}
ignoreThrows(() -> {
Method m = metaBean.getClass().getDeclaredMethod(mp.name());
m.setAccessible(true);
m.invoke(metaBean);
});
ignoreThrows(() -> {
Method m = metaBean.getClass().getDeclaredMethod(
"propertySet", Bean.class, String.class, Object.class, Boolean.TYPE);
m.setAccessible(true);
m.invoke(metaBean, bean, mp.name(), "", true);
});
}
ignoreThrows(() -> {
Method m = metaBean.getClass().getDeclaredMethod(
"propertyGet", Bean.class, String.class, Boolean.TYPE);
m.setAccessible(true);
m.invoke(metaBean, bean, "Not a real property name", true);
});
MetaProperty fakeMetaProp = StandaloneMetaProperty.of("fake", metaBean, String.class);
ignoreThrows(() -> metaBean.builder().set(fakeMetaProp, JodaBeanTests.TEST_COVERAGE_STRING));
ignoreThrows(() -> metaBean.builder().set(JodaBeanTests.TEST_COVERAGE_PROPERTY, JodaBeanTests.TEST_COVERAGE_STRING));
ignoreThrows(() -> bean.property(JodaBeanTests.TEST_COVERAGE_PROPERTY));
}
private static void assertNotNull(Map> metaPropMap) {
}
// cover parts of a bean that are not property-based
private static void coverNonProperties(Bean bean) {
MetaBean metaBean = bean.metaBean();
assertFalse(metaBean.metaPropertyExists(""));
assertThrows(() -> metaBean.builder().get("foo_bar"), NoSuchElementException.class);
assertThrows(() -> metaBean.builder().set("foo_bar", ""), NoSuchElementException.class);
assertThrows(() -> metaBean.metaProperty("foo_bar"), NoSuchElementException.class);
if (metaBean instanceof DirectMetaBean) {
DirectMetaProperty dummy =
DirectMetaProperty.ofReadWrite(metaBean, "foo_bar", metaBean.beanType(), String.class);
assertThrows(() -> dummy.get(bean), NoSuchElementException.class);
assertThrows(() -> dummy.set(bean, ""), NoSuchElementException.class);
assertThrows(() -> dummy.setString(bean, ""), NoSuchElementException.class);
assertThrows(() -> metaBean.builder().get(dummy), NoSuchElementException.class);
assertThrows(() -> metaBean.builder().set(dummy, ""), NoSuchElementException.class);
}
Set propertyNameSet = bean.propertyNames();
assertNotNull(propertyNameSet);
for (String propertyName : propertyNameSet) {
assertNotNull(bean.property(propertyName));
}
assertThrows(() -> bean.property(""), NoSuchElementException.class);
Class extends Bean> beanClass = bean.getClass();
ignoreThrows(() -> {
Method m = beanClass.getDeclaredMethod("meta");
m.setAccessible(true);
m.invoke(null);
});
ignoreThrows(() -> {
Method m = beanClass.getDeclaredMethod("meta" + beanClass.getSimpleName(), Class.class);
m.setAccessible(true);
m.invoke(null, String.class);
});
ignoreThrows(() -> {
Method m = beanClass.getDeclaredMethod("meta" + beanClass.getSimpleName(), Class.class, Class.class);
m.setAccessible(true);
m.invoke(null, String.class, String.class);
});
ignoreThrows(() -> {
Method m = beanClass.getDeclaredMethod("meta" + beanClass.getSimpleName(), Class.class, Class.class, Class.class);
m.setAccessible(true);
m.invoke(null, String.class, String.class, String.class);
});
ignoreThrows(() -> {
Method m = bean.getClass().getDeclaredMethod("builder");
m.setAccessible(true);
m.invoke(null);
});
ignoreThrows(() -> {
Method m = bean.getClass().getDeclaredMethod("toBuilder");
m.setAccessible(true);
m.invoke(bean);
});
assertNotNull(bean.toString());
assertNotNull(metaBean.toString());
assertNotNull(metaBean.builder().toString());
}
// different combinations of values to cover equals()
@SuppressWarnings("unlikely-arg-type")
private static void coverEquals(Bean bean) {
// create beans with different data and compare each to the input bean
// this will normally trigger each of the possible branches in equals
List> buildableProps = bean.metaBean().metaPropertyMap().values().stream()
.filter(mp -> mp.style().isBuildable())
.collect(Collectors.toList());
for (int i = 0; i < buildableProps.size(); i++) {
try {
BeanBuilder extends Bean> bld = bean.metaBean().builder();
for (int j = 0; j < buildableProps.size(); j++) {
MetaProperty> mp = buildableProps.get(j);
if (j < i) {
bld.set(mp, mp.get(bean));
} else {
List> samples = sampleValues(mp);
bld.set(mp, samples.get(0));
}
}
Bean built = bld.build();
coverBeanEquals(bean, built);
assertEquals(built, built);
assertEquals(built.hashCode(), built.hashCode());
} catch (RuntimeException ex) {
// ignore
}
}
// cover the remaining equals edge cases
assertFalse(bean.equals(null));
assertFalse(bean.equals("NonBean"));
assertTrue(bean.equals(bean));
ignoreThrows(() -> assertEquals(bean, JodaBeanUtils.cloneAlways(bean)));
assertTrue(bean.hashCode() == bean.hashCode());
}
// sample values for setters
private static List> sampleValues(MetaProperty> mp) {
Class> type = mp.propertyType();
// enum constants
if (Enum.class.isAssignableFrom(type)) {
return Arrays.asList(type.getEnumConstants());
}
// lookup pre-canned samples
List> sample = SAMPLES.get(type);
if (sample != null) {
return sample;
}
// find any potential declared constants, using some plural rules
String typeName = type.getName();
List