fmonniot / scala3mock

Mocking framework for Scala 3
https://francois.monniot.eu/scala3mock
Other
19 stars 1 forks source link

Cannot mock Java class with private zero-arg ctor #15

Closed benhutchison closed 7 months ago

benhutchison commented 9 months ago

mock[java.io.PrintStream] doesnt work. Unfortunately PrintStream is just the sort of class one often wants to mock, to verify output is being written.

[error] java.lang.AssertionError: assertion failed: private constructor PrintStream in class PrintStream in <no file> accessed from constructor PrintStreamMock in class PrintStreamMock in /Users/ben/trayda/modules/span_log_console_logger/test/span_log_console_loggerTest.scala

However, there are public ctors for PrintStream, they just aren't the zero-arg one, eg

public PrintStream(OutputStream out)

It would be nice to hint to scala3mock to use a specific ctor that was public, by specifying a signature somehow.. šŸ¤”

fmonniot commented 9 months ago

That's a good find. I suppose one solution would be to have scala3mock filter out private constructors when selecting the constructor when overriding the base class. Filtering based on the access type sounds like something the macro API should allow, although IĀ haven't checked if that is indeed the case. IĀ can probably find some time over the weekend to check that one out.

fmonniot commented 9 months ago

Interestingly, I do not hit the same compilation error. Instead IĀ get something a bit different, although it could still be the same root cause.

compiler error ``` [error] -- [E134] Type Error: /Users/francoismonniot/Projects/github.com/fmonniot/scala3mock/core/src/test/scala/eu/monniot/scala3mock/mock/MockSuite.scala:279:14 [error] 279 | val m = mock[java.io.PrintStream] [error] | ^^^^^^^^^^^^^^^^^^^^^^^^^ [error] |None of the overloaded alternatives of constructor PrintStream in class PrintStream with types [error] | (x$0: java.io.File, x$1: java.nio.charset.Charset): java.io.PrintStream [error] | (x$0: java.io.File, x$1: String): java.io.PrintStream [error] | (x$0: java.io.File): java.io.PrintStream [error] | (x$0: String, x$1: java.nio.charset.Charset): java.io.PrintStream [error] | (x$0: String, x$1: String): java.io.PrintStream [error] | (x$0: String): java.io.PrintStream [error] | (x$0: java.io.OutputStream, x$1: Boolean, x$2: java.nio.charset.Charset): [error] | java.io.PrintStream [error] | (x$0: java.io.OutputStream, x$1: Boolean, x$2: String): java.io.PrintStream [error] | (x$0: java.io.OutputStream, x$1: Boolean): java.io.PrintStream [error] | (x$0: java.io.OutputStream): java.io.PrintStream [error] |match arguments ((false : Boolean), Null) [error] |--------------------------------------------------------------------------- [error] |Inline stack trace [error] |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [error] |This location contains code that was inlined from MockSuite.scala:279 [error] 8 | inline def mock[T](using MockContext): T = MockImpl[T] [error] | ^^^^^^^^^^^ [error] |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [error] |This location contains code that was inlined from MockSuite.scala:279 [error] 53 |export Mocks.* [error] | ^^^^^ [error] --------------------------------------------------------------------------- [error] one error found [error] (core / Test / compileIncremental) Compilation failed [error] Total time: 6 s, completed Sep 30, 2023, 7:45:57 PM ```

It turns out that error above happen when trying to extends a class with multiple constructors without choosing one in particular, like so

class PSM extends java.io.PrintStream {
}

I got some code that can discard private constructors and build off the first public constructor. Now, PrintStream is an interesting example because it requires that its OutputStream argument is non-null, but scala3mock default to null parameters for its parameters and so we get a java.lang.NullPointerException when instantiating the mocked class.

I can see two ways forward:

  1. The first one is to assume this is a limitation of the mock system: if the mocked class expect a non-null value, then as a user you'll have to provide this value yourself. This can be done with inheritance and a local class. For your example, that would look something like
    
    class MPrintStream extends java.io.PrintStream(java.io.OutputStream.nullOutputStream())

val m = mock[MPrintStream] when(m.print(_: String)).expects("hello") m.print("hello")


2. Improve the default system of scala3mock for some known type that are often problematic when nulled. We could have a `Default` implementation for `OutputStream` and that should solve the issue for this specific type. This is a more adhoc solution, but with more interesting output long term as we find out more usual types that are required to be null by standard libraries functions.

edit: I went ahead and implemented the second option. Note that the first option is still totally fine, and probably something to resort to for non-standard types. Note that with #16 merged, you can also define your own `Default` given for a type that is not present in the library. The fixed version is available on maven central snapshots with version `0.3.2+8-b7205c5d-SNAPSHOT` if you'd like to give it a try.
benhutchison commented 9 months ago

I like option 2 too šŸ‘ I'll give it a try and report back

fmonniot commented 7 months ago

As IĀ haven't heard from you in a while, I'm going to assume this issue is fixed. I have released 0.3.2+8-b7205c5d-SNAPSHOT as 0.4.0 last month to simplify consumption of this library. Feel free to re-open if that is not the case.