diff --git a/src/main/java/ru/nsu/sd/MockBuddy/MockBuddy.java b/src/main/java/ru/nsu/sd/MockBuddy/MockBuddy.java index 98398ce..56ea627 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/MockBuddy.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/MockBuddy.java @@ -1,6 +1,7 @@ package ru.nsu.sd.MockBuddy; import ru.nsu.sd.MockBuddy.internal.MockBuddyCore; +import ru.nsu.sd.MockBuddy.internal.MockingInfo; import ru.nsu.sd.MockBuddy.internal.stubbing.Stubber; public class MockBuddy { @@ -26,4 +27,9 @@ public static T spy(Class clazz) { public static Stubber when(T obj) { return mockBuddyCore.when(obj); } + + public static T verify(T obj, Integer times) { + return mockBuddyCore.verify(obj, times); + } + } diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/MockBuddyCore.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/MockBuddyCore.java index dd3a5b7..1da7886 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/MockBuddyCore.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/MockBuddyCore.java @@ -2,6 +2,7 @@ import ru.nsu.sd.MockBuddy.internal.creation.ByteBuddyMockMaker; import ru.nsu.sd.MockBuddy.internal.handling.DelegationStrategy; +import ru.nsu.sd.MockBuddy.internal.handling.VerificationHandler; import ru.nsu.sd.MockBuddy.internal.stubbing.Stubber; public class MockBuddyCore { @@ -21,4 +22,8 @@ public T spy(T obj) { public Stubber when(T obj) { return new Stubber<>(); } + + public T verify(T obj, Integer times) { + return ByteBuddyMockMaker.verify(obj, times); + } } diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/MockingInfo.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/MockingInfo.java index 1cc80ad..d1cc122 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/MockingInfo.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/MockingInfo.java @@ -1,15 +1,24 @@ package ru.nsu.sd.MockBuddy.internal; +import ru.nsu.sd.MockBuddy.internal.handling.InvocationHolder; import ru.nsu.sd.MockBuddy.internal.handling.MockInvocationHandler; import ru.nsu.sd.MockBuddy.internal.matching.ArgumentMatcherStorage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + public class MockingInfo { private static final ArgumentMatcherStorage argumentMatcherStorage; private static MockInvocationHandler lastMockInvocationHandler; + private static HashMap> hashMap; + static { argumentMatcherStorage = new ArgumentMatcherStorage(); + hashMap = new HashMap<>(); } public static MockInvocationHandler getLastMockInvocationHandler() { @@ -23,4 +32,33 @@ public static void setLastMockInvocationHandler(MockInvocationHandler handler) { public static ArgumentMatcherStorage getArgumentMatcherStorage() { return argumentMatcherStorage; } + + public static void addObjectToHashMup(Object obj, InvocationHolder holder) { + + if (hashMap.containsKey(System.identityHashCode(obj))) { + + if (hashMap.get(System.identityHashCode(obj)).stream().noneMatch(x -> x.getMethod().equals(holder.getMethod()) && Arrays.deepEquals(x.getArgs(), holder.getArgs()))) { + List newHolders = hashMap.get(System.identityHashCode(obj)); + newHolders.add(holder); + hashMap.put(System.identityHashCode(obj), newHolders); + } else { + List newHolders = hashMap.get(System.identityHashCode(obj)); + newHolders.add(holder); + hashMap.put(System.identityHashCode(obj), newHolders); + } + + } else { + List newHolders = new ArrayList<>(); + newHolders.add(holder); + hashMap.put(System.identityHashCode(obj), newHolders); + } + } + + public static void addHolders(Object obj, List holders) { + hashMap.put(System.identityHashCode(obj), holders); + } + + public static List getListOfHolders(Object obj) { + return hashMap.get(System.identityHashCode(obj)); + } } diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Mock.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Mock.java index f748601..516682f 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Mock.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Mock.java @@ -14,13 +14,6 @@ * Minimizes repetitive mock creation code. * Makes the test class more readable. * - * Example: - *

- *
- *
- *
- * 
- * */ @Target(FIELD) @Retention(RUNTIME) diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Spy.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Spy.java index da4602c..e973a57 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Spy.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/annotations/Spy.java @@ -8,14 +8,6 @@ /** * Allows shorthand wrapping of field instances in an spy object. - * - * Example: - *

- *
- *
- *
- * 
- * */ @Target(FIELD) @Retention(RUNTIME) diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/creation/ByteBuddyMockMaker.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/creation/ByteBuddyMockMaker.java index ef13506..767f9da 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/creation/ByteBuddyMockMaker.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/creation/ByteBuddyMockMaker.java @@ -8,7 +8,9 @@ import org.objenesis.instantiator.ObjectInstantiator; import ru.nsu.sd.MockBuddy.internal.MockingInfo; import ru.nsu.sd.MockBuddy.internal.handling.DelegationStrategy; +import ru.nsu.sd.MockBuddy.internal.handling.InvocationHolder; import ru.nsu.sd.MockBuddy.internal.handling.MockInvocationHandler; +import ru.nsu.sd.MockBuddy.internal.handling.VerificationHandler; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -94,5 +96,23 @@ public static T spy(T obj, MockInvocationHandler mockInvocationHandler) { return instance; } -} + public static T verify(T obj, Integer times) { + + VerificationHandler verificationHandler = new VerificationHandler(obj, times); + + @SuppressWarnings("unchecked") + Class byteBuddy = new ByteBuddy() + .subclass((Class) obj.getClass().getSuperclass()) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.to(verificationHandler)) + .make() + .load(ClassLoader.getSystemClassLoader()) + .getLoaded(); + + // Create object without calling constructor + Objenesis objenesis = new ObjenesisStd(); + ObjectInstantiator thingyInstantiator = objenesis.getInstantiatorOf(byteBuddy); + return (T) thingyInstantiator.newInstance(); + } +} diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/InvocationHolder.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/InvocationHolder.java new file mode 100644 index 0000000..8b0b015 --- /dev/null +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/InvocationHolder.java @@ -0,0 +1,53 @@ +package ru.nsu.sd.MockBuddy.internal.handling; + +import java.lang.reflect.Method; +import java.util.Arrays; + +public class InvocationHolder { + + private final Object[] args; + private final Method method; + + private int invocationCounter = 0; + + InvocationHolder(Method method, Object[] args) { + this.args = args; + this.method = method; + } + + public Object[] getArgs() { + return args; + } + + public Method getMethod() { + return method; + } + + + public int getInvocationCounter() { + return invocationCounter; + } + + public void increaseCounter() { + invocationCounter++; + +// System.out.println(this.toString() + " counter: " + invocationCounter); + } + + public void decreaseCounter() { + if (invocationCounter > 0) { + invocationCounter--; + } + +// System.out.println(this.toString() + " counter: " + invocationCounter); + + } + + @Override + public String toString() { + return "DataHolder{" + + "args=" + Arrays.toString(args) + + ", method=" + method + + '}'; + } +} diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/MockInvocationHandler.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/MockInvocationHandler.java index e763e01..dea69de 100644 --- a/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/MockInvocationHandler.java +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/MockInvocationHandler.java @@ -1,9 +1,6 @@ package ru.nsu.sd.MockBuddy.internal.handling; -import net.bytebuddy.implementation.bind.annotation.AllArguments; -import net.bytebuddy.implementation.bind.annotation.Origin; -import net.bytebuddy.implementation.bind.annotation.RuntimeType; -import net.bytebuddy.implementation.bind.annotation.SuperCall; +import net.bytebuddy.implementation.bind.annotation.*; import ru.nsu.sd.MockBuddy.internal.MockingInfo; import ru.nsu.sd.MockBuddy.internal.matching.matchers.ArgumentMatcher; @@ -11,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.Callable; /** @@ -21,16 +19,28 @@ public class MockInvocationHandler { private final List dataHolders = new ArrayList<>(); + private List invocationHolders = new ArrayList<>(); + private Method lastMethod = null; private Object[] lastArgs = null; private final DelegationStrategy delegationStrategy; + private Object obj = null; + public MockInvocationHandler(DelegationStrategy delegationStrategy) { this.delegationStrategy = delegationStrategy; } @RuntimeType - public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllArguments Object[] args) throws Throwable { + public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllArguments Object[] args, @This Object obj) throws Throwable { + + this.obj = obj; + + invocationHolders = MockingInfo.getListOfHolders(obj); + + if (invocationHolders == null) { + invocationHolders = new ArrayList(); + } MockingInfo.setLastMockInvocationHandler(this); @@ -48,18 +58,35 @@ public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllAr lastArgs = args; } + for (InvocationHolder invocationHolder : invocationHolders) { + if (invocationHolder.getMethod().equals(method) && Arrays.deepEquals(invocationHolder.getArgs(), args)) { + invocationHolder.increaseCounter(); + } + } + + if (invocationHolders.stream().noneMatch(x -> x.getMethod().equals(method) && Arrays.deepEquals(x.getArgs(), args))) { + InvocationHolder holder = new InvocationHolder(method, args); + holder.increaseCounter(); + invocationHolders.add(holder); + } + + MockingInfo.addHolders(obj, invocationHolders); + // checks if the method was already called with the given arguments (without argument matchers) for (DataHolder dataHolder : dataHolders) { if (dataHolder.getMethod().equals(method) && Arrays.deepEquals(dataHolder.getArgs(), args)) { if (!dataHolder.isWithMatchers()) { switch (dataHolder.getDelegationStrategy()) { case CALL_REAL_METHOD: +// System.out.println("first call real method"); return zuper.call(); case THROW_EXCEPTION: +// System.out.println("first throw exception"); throw dataHolder.getToThrow(); case RETURN_CUSTOM: +// System.out.println("first return custom"); return dataHolder.getRetObj(); } @@ -78,8 +105,10 @@ public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllAr match = dataHolder.getLocalArgumentMatchersList().get(i).matches(lastArgs[i]); if (!match) { - if (delegationStrategy == DelegationStrategy.CALL_REAL_METHOD) + if (delegationStrategy == DelegationStrategy.CALL_REAL_METHOD) { +// System.out.println("let's try here when"); return zuper.call(); + } else return null; } @@ -87,18 +116,22 @@ public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllAr switch (dataHolder.getDelegationStrategy()) { case CALL_REAL_METHOD: +// System.out.println("second call real method"); return zuper.call(); case THROW_EXCEPTION: +// System.out.println("second throw exception"); throw dataHolder.getToThrow(); case RETURN_CUSTOM: +// System.out.println("second return custom"); return dataHolder.getRetObj(); } } else { // Override matcher +// System.out.println("xz what is this"); return null; } } @@ -114,6 +147,21 @@ public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllAr public void setRetObj(Object retObj) { + invocationHolders = MockingInfo.getListOfHolders(obj); + + if (invocationHolders == null) { + invocationHolders = new ArrayList(); + } + + // Decrease invocation counter because method was in MockBuddy.when + for (InvocationHolder invocationHolder : invocationHolders) { + if (invocationHolder.getMethod().equals(lastMethod) && Arrays.deepEquals(invocationHolder.getArgs(), lastArgs)) { + invocationHolder.decreaseCounter(); + } + } + + MockingInfo.addHolders(obj, invocationHolders); + // Remove existing rule with same method and arguments dataHolders.removeIf(dh -> dh.getMethod().equals(lastMethod) && Arrays.deepEquals(dh.getArgs(), lastArgs)); @@ -136,6 +184,21 @@ public void setRetObj(Object retObj) { public void setThrowable(Throwable throwable) { + invocationHolders = MockingInfo.getListOfHolders(obj); + + if (invocationHolders == null) { + invocationHolders = new ArrayList(); + } + + // Decrease invocation counter because method was in MockBuddy.when + for (InvocationHolder invocationHolder : invocationHolders) { + if (invocationHolder.getMethod().equals(lastMethod) && Arrays.deepEquals(invocationHolder.getArgs(), lastArgs)) { + invocationHolder.decreaseCounter(); + } + } + + MockingInfo.addHolders(obj, invocationHolders); + // Remove existing rule with same method and arguments dataHolders.removeIf(dh -> dh.getMethod().equals(lastMethod) && Arrays.deepEquals(dh.getArgs(), lastArgs)); @@ -158,6 +221,21 @@ public void setThrowable(Throwable throwable) { public void setRealMethodInvocation() { + invocationHolders = MockingInfo.getListOfHolders(obj); + + if (invocationHolders == null) { + invocationHolders = new ArrayList(); + } + + // Decrease invocation counter because method was in MockBuddy.when + for (InvocationHolder invocationHolder : invocationHolders) { + if (invocationHolder.getMethod().equals(lastMethod) && Arrays.deepEquals(invocationHolder.getArgs(), lastArgs)) { + invocationHolder.decreaseCounter(); + } + } + + MockingInfo.addHolders(obj, invocationHolders); + // Remove existing rule with same method and arguments dataHolders.removeIf(dh -> dh.getMethod().equals(lastMethod) && Arrays.deepEquals(dh.getArgs(), lastArgs)); @@ -177,4 +255,15 @@ public void setRealMethodInvocation() { dataHolders.add(new DataHolder(lastMethod, lastArgs)); } } + + public int getInvocationCounter() { + + invocationHolders = MockingInfo.getListOfHolders(obj); + + if (invocationHolders.stream().noneMatch(x -> x.getMethod().equals(lastMethod) && Arrays.deepEquals(x.getArgs(), lastArgs))) { + return 0; + } else { + return Objects.requireNonNull(invocationHolders.stream().filter(x -> x.getMethod().equals(lastMethod) && Arrays.deepEquals(x.getArgs(), lastArgs)).findFirst().orElse(null)).getInvocationCounter(); + } + } } diff --git a/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/VerificationHandler.java b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/VerificationHandler.java new file mode 100644 index 0000000..9cfdd99 --- /dev/null +++ b/src/main/java/ru/nsu/sd/MockBuddy/internal/handling/VerificationHandler.java @@ -0,0 +1,42 @@ +package ru.nsu.sd.MockBuddy.internal.handling; + +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import ru.nsu.sd.MockBuddy.internal.MockingInfo; +import ru.nsu.sd.MockBuddy.internal.matching.matchers.ArgumentMatcher; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; + + +public class VerificationHandler { + + private Integer times; + private Object obj; + + public VerificationHandler(Object obj, Integer times) { + this.obj = obj; + this.times = times; + } + + @RuntimeType + public Object invoke(@SuperCall Callable zuper, @Origin Method method, @AllArguments Object[] args) throws Throwable { + + if (Objects.requireNonNull(MockingInfo.getListOfHolders(obj) + .stream() + .filter(x -> x.getMethod().equals(method) && Arrays.deepEquals(x.getArgs(), args)) + .findFirst().orElse(null)) + .getInvocationCounter() != times) { + throw new IllegalArgumentException("Verification error"); + } + + return null; + + } +} diff --git a/src/test/java/ru/nsu/sd/MockBuddy/VerifyTest.java b/src/test/java/ru/nsu/sd/MockBuddy/VerifyTest.java new file mode 100644 index 0000000..ac04869 --- /dev/null +++ b/src/test/java/ru/nsu/sd/MockBuddy/VerifyTest.java @@ -0,0 +1,58 @@ +package ru.nsu.sd.MockBuddy; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import ru.nsu.sd.MockBuddy.internal.annotations.Mock; +import ru.nsu.sd.MockBuddy.internal.annotations.MockInitializer; +import ru.nsu.sd.MockBuddy.testclasses.TestClass; + +import static ru.nsu.sd.MockBuddy.internal.matching.ArgumentMatchers.*; + +public class VerifyTest { + + @Mock + private TestClass testClass; + private TestClass testClass2; + + @Before + public void setup() { + MockInitializer.initMocks(this); + testClass2 = MockBuddy.mock(TestClass.class); + } + + @Test + public void test_1() { + + // 123 || 333 + MockBuddy.when(testClass.bar(123, 3)).thenReturn(11); + MockBuddy.when(testClass2.bar(7, 3)).thenReturn(17); + + MockBuddy.verify(testClass, 0).bar(123, 3); + MockBuddy.verify(testClass2, 0).bar(7, 3); + + testClass.bar(123, 3); // 1 + testClass.bar(123, 3); // 2 + + MockBuddy.verify(testClass, 2).bar(123, 3); + + MockBuddy.when(testClass.bar(123, 3)).thenReturn(12); + + MockBuddy.verify(testClass, 2).bar(123, 3); + + testClass.bar(123, 3); // 3 + + MockBuddy.verify(testClass, 3).bar(123, 3); + + testClass.bar(123, 4); // 1 + + MockBuddy.verify(testClass, 1).bar(123, 4); + + testClass.bar(0, 0); // 1 + + MockBuddy.verify(testClass, 1).bar(0, 0); + MockBuddy.verify(testClass, 3).bar(123, 3); + + + } +}