diff --git a/demo/java-demo/src/main/java/com/alibaba/testable/demo/service/DemoMatcherService.java b/demo/java-demo/src/main/java/com/alibaba/testable/demo/service/DemoMatcherService.java new file mode 100644 index 0000000..7b3bd1f --- /dev/null +++ b/demo/java-demo/src/main/java/com/alibaba/testable/demo/service/DemoMatcherService.java @@ -0,0 +1,71 @@ +package com.alibaba.testable.demo.service; + + +import com.alibaba.testable.demo.model.BlackBox; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +/** + * @author flin + */ +@Service +public class DemoMatcherService { + + /** + * Method to be mocked + */ + private void methodToBeMocked() { + // pretend to have some code here + } + + /** + * Method to be mocked + */ + private void methodToBeMocked(Object a1, Object a2) { + // pretend to have some code here + } + + /** + * Method to be mocked + */ + private void methodToBeMocked(Object[] a) { + // pretend to have some code here + } + + public void callMethodWithoutArgument() { + methodToBeMocked(); + } + + public void callMethodWithNumberArguments() { + // named variable and lambda variable will be recorded as different type + // should have them both in test case + List floatList = new ArrayList<>(); + floatList.add(1.0F); + floatList.add(2.0F); + methodToBeMocked(1, 2); + methodToBeMocked(1L, 2.0); + Long[] longArray = new Long[]{1L, 2L}; + methodToBeMocked(new ArrayList(){{ add(1); }}, new HashSet(){{ add(1.0F); }}); + methodToBeMocked(1.0, new HashMap(2){{ put(1, 1.0F); }}); + methodToBeMocked(floatList, floatList); + methodToBeMocked(longArray); + methodToBeMocked(new Double[]{1.0, 2.0}); + } + + public void callMethodWithStringArgument() { + methodToBeMocked("hello", "world"); + methodToBeMocked("testable", "mock"); + methodToBeMocked(new String[]{"demo"}); + } + + public void callMethodWithObjectArgument() { + methodToBeMocked(new BlackBox("hello"), new BlackBox("world")); + methodToBeMocked(new BlackBox("demo"), null); + methodToBeMocked(null, new BlackBox("demo")); + } + +} diff --git a/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMatcherServiceTest.java b/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMatcherServiceTest.java new file mode 100644 index 0000000..7aa6938 --- /dev/null +++ b/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMatcherServiceTest.java @@ -0,0 +1,61 @@ +package com.alibaba.testable.demo.service; + +import com.alibaba.testable.core.annotation.TestableMock; +import com.alibaba.testable.demo.model.BlackBox; +import org.junit.jupiter.api.Test; + +import static com.alibaba.testable.core.matcher.InvokeMatcher.*; +import static com.alibaba.testable.core.tool.TestableTool.*; + + +class DemoMatcherServiceTest { + + private DemoMatcherService demo = new DemoMatcherService(); + + @TestableMock(targetMethod = "methodToBeMocked") + private void methodWithoutArgument(DemoMatcherService self) {} + + @TestableMock(targetMethod = "methodToBeMocked") + private void methodWithArguments(DemoMatcherService self, Object a1, Object a2) {} + + @TestableMock(targetMethod = "methodToBeMocked") + private void methodWithArrayArgument(DemoMatcherService self, Object[] a) {} + + @Test + void should_match_no_argument() { + demo.callMethodWithoutArgument(); + verify("methodWithoutArgument").withTimes(1); + demo.callMethodWithoutArgument(); + verify("methodWithoutArgument").withTimes(2); + } + + @Test + void should_match_number_arguments() { + demo.callMethodWithNumberArguments(); + verify("methodWithArguments").without(anyString(), 2); + verify("methodWithArguments").withInOrder(anyInt(), 2); + verify("methodWithArguments").withInOrder(anyLong(), anyNumber()); + verify("methodWithArguments").with(1.0, anyMapOf(Integer.class, Float.class)); + verify("methodWithArguments").with(anyList(), anySetOf(Float.class)); + verify("methodWithArguments").with(anyList(), anyListOf(Float.class)); + verify("methodWithArrayArgument").with(anyArrayOf(Long.class)); + verify("methodWithArrayArgument").with(anyArray()); + } + + @Test + void should_match_string_arguments() { + demo.callMethodWithStringArgument(); + verify("methodWithArguments").with(startsWith("he"), endsWith("ld")); + verify("methodWithArguments").with(contains("stab"), matches("m.[cd]k")); + verify("methodWithArrayArgument").with(anyArrayOf(String.class)); + } + + @Test + void should_match_object_arguments() { + demo.callMethodWithObjectArgument(); + verify("methodWithArguments").withInOrder(any(BlackBox.class), any(BlackBox.class)); + verify("methodWithArguments").withInOrder(nullable(BlackBox.class), nullable(BlackBox.class)); + verify("methodWithArguments").withInOrder(isNull(), notNull()); + } + +} diff --git a/testable-core/src/main/java/com/alibaba/testable/core/matcher/InvokeMatcher.java b/testable-core/src/main/java/com/alibaba/testable/core/matcher/InvokeMatcher.java index 5f10c21..8488d5b 100644 --- a/testable-core/src/main/java/com/alibaba/testable/core/matcher/InvokeMatcher.java +++ b/testable-core/src/main/java/com/alibaba/testable/core/matcher/InvokeMatcher.java @@ -97,7 +97,7 @@ public class InvokeMatcher { } public static InvokeMatcher anyListOf(final Class clazz) { - return anyClassWithTemplateOf(List.class, clazz); + return anyClassWithCollectionOf(List.class, clazz); } public static InvokeMatcher anySet() { @@ -105,7 +105,7 @@ public class InvokeMatcher { } public static InvokeMatcher anySetOf(final Class clazz) { - return anyClassWithTemplateOf(Set.class, clazz); + return anyClassWithCollectionOf(Set.class, clazz); } public static InvokeMatcher anyMap() { @@ -113,16 +113,7 @@ public class InvokeMatcher { } public static InvokeMatcher anyMapOf(final Class keyClass, final Class valueClass) { - return any(new MatchFunction() { - @Override - public boolean check(Object value) { - return value != null && - Map.class.isAssignableFrom(value.getClass()) && - value.getClass().getTypeParameters().length == 2 && - keyClass.isAssignableFrom(value.getClass().getTypeParameters()[0].getGenericDeclaration()) && - valueClass.isAssignableFrom(value.getClass().getTypeParameters()[1].getGenericDeclaration()); - } - }); + return anyClassWithMapOf(keyClass, valueClass); } public static InvokeMatcher anyCollection() { @@ -130,7 +121,7 @@ public class InvokeMatcher { } public static InvokeMatcher anyCollectionOf(final Class clazz) { - return anyClassWithTemplateOf(Collection.class, clazz); + return anyClassWithCollectionOf(Collection.class, clazz); } public static InvokeMatcher anyIterable() { @@ -138,7 +129,7 @@ public class InvokeMatcher { } public static InvokeMatcher anyIterableOf(final Class clazz) { - return anyClassWithTemplateOf(Iterable.class, clazz); + return anyClassWithCollectionOf(Iterable.class, clazz); } public static InvokeMatcher any(final Class clazz) { @@ -248,15 +239,53 @@ public class InvokeMatcher { }); } - private static InvokeMatcher anyClassWithTemplateOf(final Class collectionClass, final Class clazz) { + private static InvokeMatcher anyClassWithCollectionOf(final Class collectionClass, final Class clazz) { return any(new MatchFunction() { @Override public boolean check(Object value) { return value != null && collectionClass.isAssignableFrom(value.getClass()) && - value.getClass().getTypeParameters().length == 1 && - clazz.isAssignableFrom(value.getClass().getTypeParameters()[0].getGenericDeclaration()); + allElementsHasType((Collection)value, clazz); } }); } + + private static InvokeMatcher anyClassWithMapOf(final Class keyClass, final Class valueClass) { + return any(new MatchFunction() { + @Override + public boolean check(Object value) { + return value != null && + Map.class.isAssignableFrom(value.getClass()) && + allElementsHasType((Map)value, keyClass, valueClass); + } + }); + } + + /** + * Because of type erase, there's no way to directly fetch original type of collection template + * this could be a temporary solution + */ + private static boolean allElementsHasType(Map items, Class keyClass, Class valueClass) { + for (Map.Entry e : items.entrySet()) { + if (!(keyClass.isAssignableFrom(e.getKey().getClass()) && + valueClass.isAssignableFrom(e.getValue().getClass()))) { + return false; + } + } + return true; + } + + /** + * Because of type erase, there's no way to directly fetch original type of collection template + * this could be a temporary solution + */ + private static boolean allElementsHasType(Collection values, Class clazz) { + for (Object v : values.toArray()) { + if (!clazz.isAssignableFrom(v.getClass())) { + return false; + } + } + return true; + } + }