Skip to content

Conversation

devtaebong
Copy link
Contributor

Summary

This PR introduces a mechanism to prevent mocking of certain classes that are generally considered bad practice.
By default, attempting to mock restricted classes will log a warning. Additionally, a new configuration option has
been added to allow users to enforce strict restrictions, causing an exception to be thrown when attempting to mock
a restricted class.

Changes

1️⃣ Restricted Mocking Mechanism

  • Added RestrictedMockClasses to maintain a list of non-mockable classes.
  • By default, logs a warning when attempting to mock a restricted class.

2️⃣ New Configuration Option

  • Introduced MockKSettings.disallowMockingRestrictedClasses to enforce strict mocking prevention.
  • Setting can be configured via settings.properties:
    disallowMockingRestrictedClasses=false
  • If set to true, attempting to mock a restricted class will throw an IllegalArgumentException.

3️⃣ Configurable Restricted Class List

  • Predefined restricted classes include:
    • System
    • Java Collections (List, Set, Map, Queue, etc.)
    • I/O classes (File, Path)
    • Value objects (default list can be extended by users)
    • Users can dynamically add/remove restricted classes.

4️⃣ Integration with mockk()

  • mockk<T>() now checks for restricted classes before creating a mock.
  • Calls RestrictedMockClasses.handleRestrictedMocking() to log warnings or throw exceptions based on settings.

5️⃣ Unit Tests

  • Added tests to verify:
    • Warnings are logged when attempting to mock restricted classes.
    • Exceptions are thrown when disallowMockingRestrictedClasses = true.
    • User-defined restricted classes are correctly handled.

- Implement `RestrictedMockClasses` to maintain a list of restricted classes
- Use `isAssignableFrom` to check for restricted class types dynamically
- Allow adding and removing user-defined restricted types
- Include default restricted types: System, Collections, I/O classes
- Added `disallowMockingRestrictedClasses` property to `MockKSettings`
- Loaded setting from `settings.properties` for better configurability
- Integrated setting into `RestrictedMockClasses` to prevent restricted class mocking
- If enabled, throws `IllegalArgumentException` instead of just logging a warning
- Added a test to verify that attempting to mock a user-defined restricted class
  throws an `IllegalArgumentException` when `disallowMockingRestrictedClasses` is enabled.
- Ensures that dynamically added restricted classes are properly handled.
- Validates that the correct exception message is returned.
@devtaebong devtaebong changed the title feat: Restrict mocking of certain classes and add configuration option [#1304] feat: Restrict mocking of certain classes and add configuration option Jan 30, 2025
@Raibaz
Copy link
Collaborator

Raibaz commented Jan 30, 2025

Thanks for looking into this! Can you please run ./gradlew :mockk-dsl:apiDump and add the api dump to the PR?

userDefinedRestrictedTypes.any { it.isAssignableFrom(clazz) }
}

fun addRestrictedType(clazz: Class<*>) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the use case for changing the restricted types in code?

I'm worried that if we allow adding or removing a restricted type in code, tests will no longer be atomic.

Think of this case:

Test A mocks class Foo
Test B adds class Foo to the restricted types

Now the order of execution matters, because executing A before B passes and executing B before A doesn't, and yet tests should be independent.

How about making the user defined restricted types a configuration property?

@devtaebong
Copy link
Contributor Author

Hi @Raibaz ,

To guarantee test independence, I implemented MockingRestrictedExtension by extending BeforeEachCallback. This ensures that each test runs with a clean state.

Additionally, for better usability, I modified the API to allow users to:

  • Register custom restricted classes via annotation (@MockkRestricted)
  • Configure whether to throw an exception when mocking restricted classes directly through annotations.
    Would love your feedback on this approach. Let me know if you see any improvements!

Thanks

@Raibaz
Copy link
Collaborator

Raibaz commented Feb 3, 2025

Thanks for looking into this!

I have a few concerns about this approach, though:

  • IIUC, for mocking restrictions to work developers need to extend each test with @MockingRestrictedExtension, so if a developer tries to mock, say, System in a test that doesn't have the extension it will work without the restriction
  • I'm not convinced about registering restricted classes via annotation: first of all, MockK is usually imported in projects with a test scope, so it is usually not visible to business code classes, and besides, developers may want to restrict mocking classes that belong to libraries, or that in general that are not their own; it wouldn't be possible this way

How about adding a configuration property in MockKSettings with the list of fully qualified names of user-defined restricted classes?

That would allow you to add, in your mockk.properties file, something like:

restrictedClasses=com.foo.Bar,com.foo.Baz

I know it's more error-prone than annotating restricted classes, but I think it gives the right amount of flexibility.

WDYT?

@devtaebong devtaebong closed this Feb 4, 2025
@devtaebong devtaebong reopened this Feb 4, 2025
@devtaebong
Copy link
Contributor Author

devtaebong commented Feb 4, 2025

@Raibaz Thanks for your feedback!
I have a few questions regarding the MockKSettings-based approach for restricting classes:

Currently, when @MockingRestrictedExtension is not used, a warning log is generated when restricted classes (e.g., System) are mocked. The annotation is recommended for cases where users want to add custom classes or throw exceptions.

Given this behavior, I’d like to clarify the following regarding the mockk.properties configuration:

  1. Should users manually register restricted classes in the mockk.properties file?
  2. Will this configuration be applied globally to all tests?

Looking forward to your insights! 😊

@Raibaz
Copy link
Collaborator

Raibaz commented Feb 4, 2025

Oh, I see, that makes sense, thanks for clarifying!

So, I think that if a user restricts mocking a class in a MockKSetting, by default trying to mock it should throw an exception, or log a warning, anywhere in the application, without having to explicitly add the @MockingRestrictedExtension.

If anything, I'd like it to work the other way around: if class Foo is restricted from mocking it means you shouldn't try to mock it anywhere, but if you really really need to, you can override the restriction with a test extension, something like @AllowMockingRestrictedClasses.

What do you think about this approach?

@Raibaz
Copy link
Collaborator

Raibaz commented Feb 4, 2025

(also, thanks for the followup!)

@devtaebong
Copy link
Contributor Author

That makes a lot of sense! I see the value in making the restriction the default behavior while allowing an explicit override when absolutely necessary. Instead of relying solely on annotations, I’ll explore the possibility of managing restricted classes globally through a configuration file (e.g., mockk.properties). This would provide more flexibility while ensuring consistency across all tests.

I'll look into how this can be implemented effectively and update you with my findings.
Thanks for the suggestion!

- Introduced `MockkValidator` to enforce global mock restrictions.
- Automatically throws `MockKException` or logs a warning if a restricted class is mocked.
- Updated `RestrictMockkConfiguration` to support user-defined restricted classes.
- Added support for hierarchical type checking (e.g., restricting `Collection` applies to `ArrayList`).
- Implemented `TestPropertiesLoader` for dynamic test configurations.
- Refactored tests: separated collection-related tests into `RestrictMockkCollectionTest`.
@devtaebong
Copy link
Contributor Author

@Raibaz
I've made modifications to load the restricted classes from the configuration file. I would greatly appreciate any additional feedback you may have.

@Raibaz
Copy link
Collaborator

Raibaz commented Feb 7, 2025

I like this!

I think we can merge this, but it needs two minor improvements:

  • Can you please re-run ./gradlew :mockk-dsl:apiDump?
  • Can you please add documentation about how to use the mocking restriction in README.md?

Thanks a lot for looking into this! <3

@devtaebong
Copy link
Contributor Author

devtaebong commented Feb 8, 2025

@Raibaz

I've completed adding documentation on how to use mocking restrictions in README.md.

I'm really happy to contribute to a project I use frequently, and I appreciate all the valuable feedback you've provided. 😊
Please let me know if there's anything else you'd like me to adjust or clarify!

README.md Outdated
@Test
fun `when throwExceptionOnBadMock is true should throw MockKException for collections`() {
assertThrows<MockKException> {
mockk<List<String>>() // Throws MockException
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: should be // Throws MockKException

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve updated the comment to MockException -> MockKException as suggested. 😭

@Raibaz Raibaz merged commit e8dc7c0 into mockk:master Feb 11, 2025
22 checks passed
@Raibaz
Copy link
Collaborator

Raibaz commented Feb 11, 2025

Thanks a lot for the contribution and for the continued follow ups!

SaintPatrck added a commit to bitwarden/android that referenced this pull request Mar 4, 2025
Update mockk from 1.13.13 to 1.13.17

<details>
<summary>mockk/mockk (io.mockk:mockk)</summary>

### p`v1.13.17`](https://redirect.github.com/mockk/mockk/releases/tag/1.13.17)

[Compare Source](mockk/mockk@1.13.16...1.13.17)

##### What's Changed

-   Fix(Issue #1333): Bug fix for the issue with MockK 1.13.16 Wraps Results Objects Twice by @kpadhiamex in [#1334](mockk/mockk#1334)
-   Fix (issue# 1329) parallel testing for unmockkAll by @kpadhiamex in [#1335](mockk/mockk#1335)
-   [[#1304]](mockk/mockk#1304) feat: Restrict mocking of certain classes and add configuration option by @devtaebong in [#1340](mockk/mockk#1340
-   Update README.md - Clarify that private fields cannot be mocked by @p4ulor in [#1347](mockk/mockk#1347)
-   Added new property "failOnSetBackingFieldException" to fail test if a backing field could not be set by @cgm-aw in [#1349](mockk/mockk#1349)
-   Fix compilation error in constructedWith docs by @TWiStErRob in [#1354](mockk/mockk#1354)

**Full Changelog: **Full Changelog**: mockk/mockk@1.13.16...1.13.17

### [`v1.13.16`](https://redirect.github.com/mockk/mockk/releases/tag/1.13.16)

[Compare Source](https://redirect.github.com/mockk/mockk/compare/1.13.14...1.13.16)

##### What's Changed

-   Fix( Issue [#&#8203;1073](https://redirect.github.com/mockk/mockk/issues/1073)): Bug fix for the issue with mocking value classes with coEvery by [@&#8203;kpadhiamex](https://redirect.github.com/kpadhiamex) in [https://github.com/mockk/mockk/pull/1332](https://redirect.github.com/mockk/mockk/pull/1332)

**Full Changelog**: mockk/mockk@1.13.14...1.13.16

### [`v1.13.14`](https://redirect.github.com/mockk/mockk/releases/tag/1.13.14)

[Compare Source](https://redirect.github.com/mockk/mockk/compare/1.13.13...1.13.14)

##### What's Changed

-   fix(1308): Handle nullable complex and nested value classes by [@&#8203;VasilisDrettas-tomtom](https://redirect.github.com/VasilisDrettas-tomtom) in [https://github.com/mockk/mockk/pull/1314](https://redirect.github.com/mockk/mockk/pull/1314)
-   Fix(Issue no. 1330) for Relaxed Mocking Value When Property is Nested Value Class by [@&#8203;kpadhiamex](https://redirect.github.com/kpadhiamex) in [https://github.com/mockk/mockk/pull/1331](https://redirect.github.com/mockk/mockk/pull/1331)

##### New Contributors

-   [@&#8203;kpadhiamex](https://redirect.github.com/kpadhiamex) made their first contribution in [https://github.com/mockk/mockk/pull/1331](https://redirect.github.com/mockk/mockk/pull/1331)

**Full Changelog**: mockk/mockk@1.13.13...1.13.14

</details>
@SimonMarquis
Copy link
Contributor

SimonMarquis commented Apr 4, 2025

@devtaebong thanks for this feature 🙏
Unfortunately, relying solely on a mockk.properties test resource makes it painful to use on a large multi-module Gradle project.

What would you recommend for this kind of scenario?

  • Generating an extra Task for each test Task to package the generated files? (this works without any changes from mockk)
  • Having default values coming from test system properties could be an option?

@devtaebong
Copy link
Contributor Author

devtaebong commented Apr 5, 2025

@SimonMarquis Glad to hear you’re making good use of the feature!

mockk.properties is designed to be defined explicitly within each module. So in multi-module projects, you’ll need to place the file under each module’s test resources if you want module-specific configurations.

That said, it might be possible to set common defaults across modules using JVM system properties (e.g., via -D options). This could be a more scalable approach for large projects, but I’m not 100% sure if all settings from mockk.properties are respected via system properties — it would need some verification.

@nishatoma
Copy link

@devtaebong Thank you so much for this work! I've tested it as well on multiple modules, and it looks like you can actually centralize the mockk properties file!

Some things I've observed while applying this change on a projects with big files: Lots of tests are failing now since naturally, there are some tests that mock a collection for example.

Would you consider to introduce an option to gradually increase the restricted classes as opposed to starting with a full list?

For example, the idea is that for big orgs, we would start with no restrictions, and then over time, we can start adding collection, Map, etc.

The current state is that there are a lot of tests that will fail, so this feature would need to be all/ or nothing if you have failing tests.

Another nice thing, is it would be really helpful and good QoL change to be able to isolate this feature for specific tests i.e Robolectric vs. pure JVM tests!

Let me know what you think, thank you again, really great work!

@MarekMacko
Copy link

Could you share, @nishatoma, how you centralized the mockk.properties file?

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