Now, every time I change the manager's Public API, I have to update the interface on cli, otherwise my tests in cli will pass even though nothing will then work:
cli app stucture
.
├── README.md
├── lib
│ ├── cli.ex
│ └── manager.ex # interface for the manager umbrella app
├── mix.exs
└── test
├── cli_test.exs
├── support
└── test_helper.exs
Problems
So, following this pattern, I have a few issues:
every time create a new @type in the manager, I have to update all the interfaces that use it, so the projects using manager as a dependency can have access to the new types I created.
every time I change manager's public API, I have to update the projects that use it as a dependency
I have a manager.ex file in my cli app, that is just an interface for the manager app. This is confusing, as people reading my code will not intuitively understand this is just an interface so i can create a mock that obeys it (mocks as nouns :D)
Possible solutions?
Given these issues I have considered the following option:
create a new app, called interfaces, where I define the interfaces of all umbrella apps.
This would have the following benefits:
Changes to the Public API of any app would always be reflected there.
All dialyzer @types would be there as well
Any project using the Interface app as a dependency would have immediate access to the most updated interfaces and type specs.
I would get rid of the manager.ex file because I would import it from the interface app
It would however have the following drawbacks:
Any umbrella app would be forced to have 2 dependencies: the interface app, and the app they want to use (in this case, cli would have to import both interface and manager)
They would have access to all interfaces and type specs, even if not needed
I would still have to change things in two places: the interface app, and the app that implements the interface.
So I am not really sure on how to deal with this issue.
I guess we already had this conversation, but I personally would not mock internal dependencies in the first place. But manager.ex is one file in :cli, which belongs to :cli and is built to its needs. What exactly would you be copying from :manager and why?
manager.ex is a behaviour, that represents the public API of the manager app.
The manager app is an implementation of the manager.ex behaviour.
I have this behaviour so I can mock its implementation with Mox.
How would you solve this without mocks? Inject the dependencies directly?
You would end up with the same issue in the end, in the sense that a change in manager would always require another in cli.
I have a feeling this discussion comes up often in different ways because I have not yet cemented a solution Or perhaps this is something more general and I am not seeing the full picture.
So either :cli depends on :manager and takes what it gets or :manager depends on :cli and implements the interface :cli prescribes. If you do both you’re in trouble. You don’t want circular dependencies.
Also what you’re seeing here is partly the cost of decoupling things, just that you’re not yet seeing any benefits out of that, because you’re only doing it for mocking and not for general decoupled parts in your system.
If there’s no need on the manager side I’d test :cli by letting it call into :manager directly. No need to switch implementations without good reason. I also wouldn’t opt for an “interface” app yet. If you actually decouple those apps at some point (no hardcoded dependencies anymore) a shared interface might make sense.
Those apps are decoupled because my manager can have different types of interfaces. cli is just one of them, I will eventually add a phoenix-live interface that talks to the manager.
This is way decoupling was something I did in this small project.
If I allow cli to call manager directly, then I will be doing end to end tests only, that is not my objective. Or perhaps I miss understood you?
Yeah, but :cli doesn’t need to be able to interact with multiple backends I imagine. :manager is the only one it interacts with. So no decouling in that direction.
So what? To me this is not a problem in itself. If there’s something in manager, which makes testing hard, this might change. But for that fact of the part making it hard, not because I want to not have end to end tests.
Don’t know if it helps, but here are my 2 cents how we solve it.
We have the following umbrella structure:
.
├── apps
│ ├── api_client
│ └── app
api_client exposes this function ApiClient.Companies.list app uses the api_client app as a dependency (in_umbrella: true).
Now let’s say we are writing tests for app that do something with what ApiClient.Companies.list returns. First we create a wrapper for ApiClient inside the app. So we never call ApiClient.Companies.list outside of this wrapper directly. This is also where we introduce a new behaviour and use Mox to create a mock for it.
I have used a similar design where a behaviour is defined for each Umbrella app’s public API module and the implementation module is configured via the Mix environment. For :dev and :prod it uses the default implementation, for :test it uses a Mox mock.
This allows the tests within one app to stub responses for calls to any other app. You can also use Mox’s stub_with/2 function to forward calls to the mock on to the actual implementation if necessary. This can be a useful approach where you can mock some calls, but allow other calls to be handled as normal.
After reading through this discussion I realized I had a circular dependency issue, which I have now fixed with the knowledge gained from reading this thread.
Thank everyone!
The solution I opted to go with was to use create a behaviour in the manager module that exposes its Public API and then import it in the applications that need it.
This does bring up another question, but other than that, I’m all good
Personally I’d be really happy if something similar could be introduced in Elixir apps (either from an umbrella app perspective / dependencies perspective).
@WFransen An overall interesting idea that forces you to think about your project in a different way. I believe this is not needed for more experienced developers, but for teams that are new to projects or that change constantly I believe it to be quite a nice tool.
In my specific case, it wouldn’t help much, but I see the value in it.