Assume you have a class C
. C
defines a public method (member function) C::m1
.
Calling c.m1()
(c
is an instance of C
) can either
- Return after mutating the object
c
it was called on. - Throw an exception of type
E
and leavec
unchanged.
Because of (1), C::m1
cannot be marked const
. However, if C::m1
throws it should (by the specification and by the caller's sanity) behave as if it were. I will assume no language has native support for a const-if-throws
modifier.
A way of testing this is to make a copy of c
(call it oldC
), call m1
on c
in a way in which it throws, and to test that c == oldC
after the exception is caught. However, this requires an equality operator that would otherwise not exist (and, as a consequence, adds to the amount of code that needs to be written, documented, tested, and maintained).
Assume that adding a predicate C::willM1Fail
that is const
is impossible because m1
might fail because of external factors (other servers) and it is impossible to guarantee that the next call to m1
will succeed.
Also, assume that making m1
return a new object of type C
and marking it const
instead of mutating the object it was called on (which is an obvious solution) is undesired as m1
will virtually always take path (1) and copying C
objects in production is bad, because of the performance penalty and the memory ownership complexity it will add.
The question then is: should the equality operator be defined, the exception forced (through mocking) and equality tested, or is there a better way to test that when an object of a class C
throws an exception of type E
it does not change the object (at least in the tested code paths)?
If you have a suggestion about a better design (that makes testing this property simpler), it would also be welcome.
Addressing comments
I thought this question was sufficiently language-agnostic, however, as it was pointed out in the comments by multiple individuals, the wording of this question makes it fairly clear that it was about a C++ case. I will be marking it as C++ even though it seems to be realizable to a good extent in Rust as well (&self
/&mut self
and not mutating the object when an error is returned).
There were also suggestions about changing some of the wording (const, copy, equals) into something more language-agnostic (immutable, clone, compare). Because the answers used the wording in the original question, I think it is better to leave it as it is.
Solution used
In the case that provoked this question, the solution adopted was adding another method tryResolvingForM1
, marked as const
, that throws or returns a new type D
that can be passed in a call to m1
.
The main downside is that the caller now has to call both tryResolvingForM1
and m1
. Testing was simplified as m1
can only be called if you have a D
object and it will not fail. Trying to get a D
object may fail but is guaranteed to not mutate the object it was called on.
const
as language agnostic before.