JMS is not a brilliant technology. It gets stuff done but it's not brilliant. Issues I have with JMS:
- There is the old API which is closely modelled after JDBC, quite verbose, many objects that you have to close (not that bad anymore since Java 7) and checked exceptions. You almost never want to work with this API directly but use some convince layer above it. For example something like Spring JdbcTemplate, unfortunately Spring JmsTemplate hasn't received any love and therefore doesn't support generics or functional interfaces. I have a PR open for this but nobody seems to care about JMS anymore. But at least the old API works with connection pooling.
- There is the new API (JMSContext) which is much nicer to use, much less verbose and uses runtime exceptions. Unfortunately the new API does not work well with connection pooling. That's why Spring doesn't support the new API. That was supposed to be fixed in JMS 2.1 (https://github.com/javaee/jms-spec/issues/126) but then Oracle cancelled JMS 2.1.
- The old and new API from above are both part of JMS 2.0. In addition some methods you can only call when running in Java EE, some methods you can only call when running in Java SE, and some methods you can call from both Java SE and EE. We're talking about methods on the same interface here. How can you tell which ones you can call? If you're lucky it's mentioned in the method comment, that should now be the case for most of the methods since JMS 2.0, otherwise you just have to know about the inner workings of JMS and Java EE.
- JMSException#getCause() is unspecified. Only JMSException#getLinkedException() is specified. Yes, Java 1.4 Exception#getCause(). Which means if you have some generic exception handling code that uses Exception#getCause(), like let's say a logger framework that renders stack trace, then that code is only guaranteed to work if it special cases JMSException. That was also supposed to be fixed in JMS 2.1 (https://github.com/javaee/jms-spec/issues/113) but then Oracle cancelled JMS 2.1.
- The only way to receive messages without blocking is to use an MDB. Unfortunately MDBs have to be pooled. In addition there is no portable way to connect an MDB to a non-default RAR in your application server.
Sure, JMS solves problems but brilliant it is not.
JAX-RS even has it's own component and DI model different from CDI because of NHI syndrome.
Pretty much, exactly my point about "weight of legacy technologies" :D
> JAX-RS even has it's own component and DI model
Yep, CDI and JAX-RS came out in separate JSRs. JAX-RS 2.0 could pretty much be written on top of CDI however
It's that or factory factory factories or sticky tape. Those are much worse.
All the Java EE tech is just a set of contracts really with some canned implementations that magic object glue sticks together.
No one does that now. Someone does some thinking, writes a strong interface, then builds the code behind it. There's usually only one implementation. The advantage comes at the testing phase when you have strong interfaces to mock.
I can count on one hand the amount of things we do with multiple inplementations and that's mainly switching cache and file store implementations out depending on who paid for what.
I prefer composition through more general abstractions like iterators, consumers, functors etc. where possible. With more general abstractions, there's both less need to mock (there are concrete implementations that are simple enough to use in tests) and the contract is trivial enough to be implemented consistently.
Converting control flow (where modules interact through a rich protocol of method calls) into data flow (where modules interact by consuming data and producing data) promotes both a more functional style, and naturally leads to more testable code with fewer of the downsides of mocks. It also reduces the number of interface-implementation redundantly paired types throughout the system.
Most people use them glorified stubs for returning data. Personally these days I just in memory datastores.
Without the assertion, you are left with an assumption, which from experience isn't reliable and keeps you up at night.
If you start testing the internals of function, it makes it very difficult to refactor without breaking tons and tons tests.
We only test on publicly exposed functions, and check the changes on the data or external services.
It means we can do major refactoring's of internals, keep public functions the same but tests will still pass.
One of the biggest mistakes that happened was that kent beck was misinterpreted and Unit was considered a code unit, when really it means a unit of behaviour.
Mocking the state sounds mandatory to test the method implementations in such a paradigm.
What you propose seems more like functional programming oriented testing.
- Make it possible to introspect the state and effects of the code on your dependency in the real implementation, and use that instead of a mock. Can then put the assertion into the code (a post-condition, design-by-contracts style) so it always runs, not just for the few explicitly written unit-tests. Those unit tests can be deleted in favor of something that tests something useful, like running the logic on
- Return the effect (messages to be sent) instead of doing it internally as a side-effect. Then introspect and verify this data in the test. The dependency on the message queue can now be elimitated, both in test and by the complex piece of logic
While I respect your opinion, it misses three realities. Firstly you can't generalise most complex systems enough for this to be a viable solution universally. Secondly, skill level just isn't on the market to implement this for every case even if you could specify it. Thirdly, to solve the first two problems it costs more.
Data flow moves all the test responsibilities into integration testing, which is much harder!
It's a careful thing to balance but I'm currently on the side of mocks and control flow based systems. They are cost effective and scale well in practice (I think we're on about 4 million or so lines of code and tens of thousands of class levels now)
Complex systems only become tractable when modularized, and simple module boundaries are preferable to chatty APIs; the nature of the system may make it more or less worthwhile, I'll agree.
Building crappier software because that's all you can build with the people you can find seems to me to be putting the cart before the horse. And I think you easily end up with crappier, less flexible software if you blindly follow Java norms. The tendency to over-specified tests both increases the cost of writing test code, increases the cost of refactoring (the tests usually need to change in some proportion to the implementation, even if the behaviour is no different), but even worse: your modules are, by default, not recomposable. When things are built out of data flow instead of control flow, you get a bunch of power almost for free.
Parallel and distributed processing, logging and system introspection, up to and including new product development using known good components. The business agility is especially important.
I've seen codebases calcified by "long land borders" between modules, with value locked in by the architecture, requiring risky and costly refactoring, rewrites or monotonically increasing complexity to try and repurpose that value. A system that looks more like Lego than a series of custom crafted blocks is more valuable, even if it's harder to build.
The opportunity cost obviously depends heavily on the domain. For an internal corporate IT project, I can certainly see that the upside isn't as clear.