Using [weakdeps] and [extensions] to carry on a test module

For some packages it is useful to have a TestModule that defines functions to generate data, for example. It is many times interesting that these functions are available for the package user, particularly to provide examples of the package usage.

When the TestModule gets complex, and particularly when it depends on other packages that are not main deps of the package, carrying the module within the package becomes bad for loading times.

With the new [weakdeps] and [extensions] that may be solved, and I would like to hear thoughts about this possibility:

1. Create the package MyPackage, containing a TestModule, which may depend on a heavy dependency:

module MyPackage
...
include("./test/TestModule.jl")
end
# ./test/TestModule.jl
module TestModule
using Plots # heavy!
...
end

2. Create a dummy MyPackageTests package, basically empty, and register it.

3. Have the Project.toml of MyPackage to be:

name = "MyPackage"
version = "0.1.0"
uuid = "..."

[weakdeps]
MyPackageTests = "d38c429a-6771-53c6-b99e-75d170b6e991"

[extensions]
Plots = "MyPackageTests" # only loaded if MyPackageTests is loaded

[compat]
MyPackageTests = "0.1.0"
Plots = "0.6.2"

Result:

One can develop MyPackage only all the time, and use/update the functions of TestModule easily. Also, all the functions there can be provided as example/test functions for users, such as:

using MyPackage: foo
using MyPackageTests # loads all dependencies of TestModule

data = MyPackage.TestModule.generate_data()
MyPackage.foo(data)

Is there any obvious downside of this? Having to register a dummy package seems overkill here, but not a major problem.

The TestModule package will never be loaded from within the functions of MyPackage, such that the implications for loading time of MyPackage are minimal (if any?). Or is this wrong?

Yeah, no, there will not be a bunch of empty packages getting registered. The current situation might be non-ideal for the use-case here (I must admit I did not fully understand it), but anything that requires registering “basically empty” packages is just unworkable and unscalable.

4 Likes

After reading again the new extensions docs and understanding it better, I think that this is fine, then:

name = "MyPackage"
version = "0.1.0"
uuid = "..."

[weakdeps]
Plots = "d38c429a-6771-53c6-b99e-75d170b6e991"

[extensions]
MyTestModule = "Plots" # only loaded if MyPackageTests is loaded

[compat]
Plots = "0.6.2"

and define MyTestModule in ext/MyTestModule.jl.

Plots will only be loaded if MyTestModule is loaded explicitly, which does what I want there. This is useful when functions defined for testing are also useful outside the scope of testing (as for generating data for examples, or even for debugging), as the function are available to be used in general without carrying the dependencies that are not really necessary for the main functionality of the package.

With the TestItems package, which I find very useful, now one can do:

@testitem "foo" begin
    using MyPackage: foo
    using MyPackage.MyTestModule: generate_data_for_foo
    @test foo(generate_data_for_foo()) == result
end

with MyTestModule carrying everything needed to test, which might not be part of the package itself.

Rather the opposite, MyTestModule will be automatically loaded if Plots is also loaded.

Oh, I see… Got confused about that indeed. Then well, my first interpretation was correct, and for this purpose it is less practical, without the dummy package.