Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move LocationFactory to plugins to allow overriding it #3063

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

DerFrZocker
Copy link
Contributor

As part of a rework on a project I'm contributing to, Mockito was added to replace the use of InvocationHandler.
One particular test class validates the behavior of around 1000 objects, with each other, resulting in a particular set of code running over 1 million times, using the mock a few times in the process. This results in the mock object being used over 15 millions times.

Oversimplified current use:

public class TestClass {

    // Takes 1 minute 2 seconds with normal Location logic
    // Takes 17 seconds with dummy location factory extensions
    @Test
    public void test() {
        Object object = mock();
        when(object.toString()).thenReturn("Hello");

        for (int i = 0; i < 15000000; i++) {
            assertEquals("Hello", object.toString());
        }
    }
}

Resulting in the execution time of this particular test class to rise from a few seconds to over 2 minutes. After some profiling, we found that a huge part is caused by Mocktio's Location feature.

This PR moves the LocationFactory over to plugins, to allow others to override it. Which in our case we would use to return a dummy empty Location. Using a dummy Location reduced the execution time of our test class to under 1 minute. And after some other changes in our code base, not related to this PR, to around 20 seconds (which will it is not a few seconds as before, is much better than 2 minutes).

Will we understand and are thankful that Mockito wants to provide helpful error messages, in our particular use case we would like the faster testing time, so that this test can also be easily run during normal development. If the need for Location information arise, we could still just remove the extension and run it slower for a particular time.

Looking at some GitHub issues and PR, such as #2723 which also wanted to add a system property to use a dummy Location, it seems we are not the only one which would benefit from such an extension option.

Checklist

  • Read the contributing guide
  • PR should be motivated, i.e. what does it fix, why, and if relevant how
  • If possible / relevant include an example in the description, that could help all readers
    including project members to get a better picture of the change
  • Avoid other runtime dependencies
  • Meaningful commit history ; intention is important please rebase your commit history so that each
    commit is meaningful and help the people that will explore a change in 2 years
  • The pull request follows coding style
  • Mention Fixes #<issue number> in the description if relevant
  • At least one commit should mention Fixes #<issue number> if relevant

@codecov-commenter
Copy link

codecov-commenter commented Jul 15, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 85.45%. Comparing base (cf8b820) to head (be64f3f).
Report is 237 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #3063      +/-   ##
============================================
- Coverage     85.45%   85.45%   -0.01%     
- Complexity     2888     2889       +1     
============================================
  Files           329      329              
  Lines          8801     8811      +10     
  Branches       1093     1093              
============================================
+ Hits           7521     7529       +8     
- Misses          992      993       +1     
- Partials        288      289       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@DerFrZocker DerFrZocker force-pushed the location-factory-plugin branch from dba1f01 to be64f3f Compare July 20, 2023 11:52
@TimvdLippe
Copy link
Contributor

Yes this is an area where we have not yet figured out how we should solve this. One question I have before we analyze the various options: are you sure that on the slow path you are using the Stackwalker? We have seen significant improvements when that's used, to the extent that the requests for improved performance mostly were gone.

@DerFrZocker
Copy link
Contributor Author

Yes, StackWalker is used on the slow path, I also just tested it with the Java 8 Location implementation and there it is even worse with 4 minutes and 44 seconds.

StackWalker: 1 min 58 sec
Java 8 / Throwable: 4 min 44 sec
Empty Location: 40 sec

@md-5
Copy link

md-5 commented Oct 6, 2023

Bumping this. Without some way to disable this, mockito is an order of magnitude more expensive than a simple Java proxy

@TimvdLippe
Copy link
Contributor

I do think this is a legitimate issue to be fixed, but I am unsure whether this is the best way to solve it. Ideally, I would like to explore how to best integrate this with CI/IDE tooling, so that they can take advantage of this. E.g. in case a test passes, we don't need all of this information. But when a test fails, we do need this information. I can imagine a workflow where a test fails, on rerun we want more explicit information. And on CI I can imagine that we want the full stacktrace, to ease debugging. By making it overridable, we do have the building blocks, but the way to use it is not straightforward. Similar to what we have done with mockito-inline, we need a low entry barrier here so that we can make Mockito perform the best in each scenario, while still providing sufficient information for debuggability.

Given this feature, in which scenarios do you think it is worthwhile to drop the location information, keeping in mind we have to balance performance and debuggability in for example CI environments?

@md-5
Copy link

md-5 commented Sep 11, 2024

Given this feature, in which scenarios do you think it is worthwhile to drop the location information, keeping in mind we have to balance performance and debuggability in for example CI environments?

Pretty much all local tests on my IDE, if they fail and need the location information to debug that, then I can re-enable as necessary.

The location information takes the time for certain tests from seconds to minutes

Copy link
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is predominantly useful when builds fail and there is benefit of a run-twice scenario, I wonder if we should write a Gradle/Maven plugin for this. What I am thinking is that, similar to the Gradle retry plugin, we can instruct Mockito to run without location on a first run. If a test fails, we can enable location data when we run that test and try again.

This means that we make it configurable per test invocation. The plugin should then generate a LocationFactory that can be used while running the test.

While I am typing this, I realize that integrating a pre-defined LocationFactory at runtime into a test invocation might not be possible. I don't know whether it is possible to change System properties on the fly, but maybe that is the better route after all?

Basically, I don't want this to become the default, as it significantly increases debugging efforts when dealing with failures. But for passing tests, these kind of optimizations make a lot of sense to me. Hence my thought process for retry, where you benefit from the speedup for all passing tests and then for failures it can become more descriptive. Since it is opt-in with a plugin, the default experience of Mockito remains correct. And for super slow tests, the correctness is probably more important than for fast tests.

/**
* Describes the location of something in the source code.
*/
@NotExtensible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to make this part of an extension point for external users. E.g. while Mockito is allowed to implement such interfaces, we don't want external users to implement them. Therefore, let's remove this here and create a utility function that can create a Location based on a SourceFile?

@DerFrZocker
Copy link
Contributor Author

Given that this is predominantly useful when builds fail and there is benefit of a run-twice scenario, I wonder if we should write a Gradle/Maven plugin for this. What I am thinking is that, similar to the Gradle retry plugin, we can instruct Mockito to run without location on a first run. If a test fails, we can enable location data when we run that test and try again.

This sounds definitely interesting, how would you like to taggle this? Should I make a PoC? or what is the process in Mockito for such things?

Something which also maybe worth looking into is a JUnit 5 extensions, since it is possible to intercept the method invocation it may be possible to call the test twice if it fails, although here it is harder to handle cleanup stuff.

@TimvdLippe
Copy link
Contributor

Before we invest a lot of time into writing such a plugin, let's first design it to see what is possible. Some initial thoughts/questions:

  1. Users shouldn't have to change test source code to get the behavior. E.g. a separate extension would not be ideal to me
  2. This should be an optional optimization, since rerunning tests can have unintended side-effects or a performance hit (if the amount of time spent in Mockito is vastly smaller than the rest of the test)
  3. Can we only rerun tests when Mockito fails and any other exception we ignore? Based on what can we determine whether the location info is missing and can we only rerun tests in those cases?
  4. If we go down the route of integrating with build systems, which ones do we choose and how much effort is it to keep up? Do we keep that as part of this repository or do we make it a separate project similar to mockito-testng?

@Kramermp
Copy link
Contributor

Kramermp commented Oct 11, 2024

I have a very similar situation and would like to see some type of change to enable the squashing of the reporting the location. I'm wondering if this approach / some of the options might be over-engineering this problem. In my particular use-case, I only really care about squashing the location for a specific mock that my test interacts a lot with. I (largely) do not care on any of the other mocks and fine with the performance hit. Because of this, I think it might make sense to implement this feature via the MockSettings rather than via a Plugin. This setting could also be applied to stubOnly() mocks as I think it would fit with the theme of a stubOnly mock and give users the performance increase without and code changes. This approach could yield a pretty simple solution that would see the performance uplift when it is important and would retain the StackTraces when the performance is not important. It could be leveraged simply via:

Object ex = mock(Object.class, withSettings().noLocation());

and then in MockMethodAdvice#advice, the mock settings could be checked to determine whether to report the actual location.
ex:

if mockHandler(instance).getSettings().disabledLocationReporting()
    return new ReturnValueWrapper(
        interceptor.doIntercept(
                instance, origin, arguments, realMethod, new Location () {
                    String getLocation() {
                        return "This mock has disabled location reporting" + mockHandler(instance).getSettings().getName();
                    }
                });
else
    return new ReturnValueWrapper(
        interceptor.doIntercept(
            instance, origin, arguments, realMethod, LocationFactory.create(true)));

@Kramermp
Copy link
Contributor

Looking at this further, I think it is actually incorrect to be collecting the real Location on stubOnly mocks. As far as I can tell, Location is only reported when a verification fails. With StubOnly mocks, mockito already throws an exception when attempting to verify on a stub only mock, so I don't think it is possible to get mockito to report these locations OOTB and collecting these locations is wasting resources.

This approach has the added benefit of still supporting calls into LocationFactory when a user could fail a verification check and would want to report the location. This also does not affect the reporting of UnfinishedStubbingException for stubOnly mocks so we could still vend location information on stubbed mock that is being miss used.

I suspect that in many of the situations that a user is interacting with a mock millions of times the mock is going to already be a StubOnly mock because of the memory issues of tracking millions invocations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants