1

I ran into this when I tried to write unit tests for a move-only class. I don't know how to write a test which checks if move operations actually moved the class's data members. I included here a simplified example of a class that parallels what I am working on. In reality that class supports a few more operations and (std::multimap) data members which shouldn't be relevant here.

#include <vector>

class MyClass {
 public:
  inline MyClass() = default;

  MyClass(const MyClass& other) = delete;
  MyClass(MyClass&& other) = default;

  MyClass& operator=(const MyClass& other) = delete;
  MyClass& operator=(MyClass&& other) = default;

  ~MyClass() = default;

  inline void set(const std::vector<MyStruct>& data) {data_ = data;}
  inline const std::vector<MyStruct>& get() {return data_;}
 private:
  std::vector<MyStruct> data_;
};

MyStruct contains only primitive data types (int, float and double) and some std::string types as (public) data members. For completeness, I added a definition for MyStruct at the end.

I am not sure how to even begin and couldn't find anything by searching online. Ideally, a googletest or googlemock solution would be great, but just a general approach or how it is done in other testing frame works might help me understand it and implement it in my preferred framework.

#include <string>

struct MyStruct {
  int foo;
  float bar;
  double baz;
  std::string fips;
};

Solutions so far (from comments and answers below):

Possible approach 1

Based on my exchange with @MutableSideEffect

Mock data members and test if their move operations are called when MyClass's move operations are called.

This seems simple and straightforward. This indicates whether the (defaulted) move operations of MyClass used the move operations of each of the data members. It should be the responsibility of the data members' types to provide proper move operations.

The only issue I have is that I don't know how to mock data types whose implementations I don't have access to without template-izing my entire code (as described in the google mock docs for mocking non-virtual functions).

In this particular case, though, I could still mock MyStruct and test whether its move operations are called when MyClass's move operations are called.

Possible approach 2

Based on the answer by @Eljay.

Add a data member, say Marker mark, to every class I defined, or to every class for which I want to know whether or not their data members are moved when the class's move operations are called. Instruct the Marker class to record in an internal state which of its constructors lead to its construction. Then test whether move operations of MyClass lead to internal state of its data member mark reflecting that it was moved.

I am not sure how fully this approach tests that all data members were moved and not only the mark data member. Also, this reminds me a lot to just mocking the data members which goes back to approach 1.

Jasper Braun
  • 109
  • 8
  • You can't mock operator overloads https://stackoverflow.com/questions/43796479/how-to-use-gmock-mock-method-for-overloaded-operators. Not sure if the provided solution in the above link works for you. – Empty Space May 14 '20 at 16:42
  • @MutableSideEffect Thanks. I believe mocking the move operations will test whether or not they are called when expected. That means I will know that they are defined and can test when they are actually called. However, will I be able to know that they actually ***moved*** the object? – Jasper Braun May 14 '20 at 16:56
  • How much can you instrument the actual class for testing purposes? You could make a `struct Marker final { ... };` object which keeps track of its state as having been constructed, moved, destructed, then add a public `Marker mark;` variable as a state tracker for testing purposes. – Eljay May 14 '20 at 16:58
  • @JasperBraun You are using builtin/stl types everywhere and hence I doubt you need to check if they are moved properly. I believe just checking if the move constructors/assignment operators of the types you defined are called is sufficient. – Empty Space May 14 '20 at 17:00
  • @Eljay I am not sure I understand correctly. How would the Marker object keep track of its state? Would I write move operations for it which modify its internal state (which is indicated by a data member of `Marker`)? Move operations being then called on the `mark` data member of `MyClass` implies automatically that move operations where also called on any other data members of `MyClass`? – Jasper Braun May 14 '20 at 17:06
  • @MutableSideEffect I was under the impression that any public method should be unit tested. Also, how would I then test move semantics on an object that has `MyClass` as data member. Would I assume that transitive dependence on only the standard library makes it save for me to assume it'll work? what if non-STL data members would be added later on to `MyClass`? I feel that would cause issues for any class that includes `MyClass` as data member. – Jasper Braun May 14 '20 at 17:09
  • @JasperBraun I don't see any point in testing move methods which are generated by the compiler (you marked them default). Mocking it only makes sense when let's say moving an object of type MyClass is an expected/required side effect of some other operation on some other class in your code. Otherwise, it is simply not worth the effort, IMO. – Empty Space May 14 '20 at 17:14
  • @MutableSideEffect Thanks. Moving objects of type `MyClass` is indeed a required side effect of other operations. `MyClass` objects are supposed to be inserted into a `std::map` via `std::map::insert` and I hope move semantics will make this more efficient. If i then test that calling `std::map::insert` actually call move operations of `MyClass` via mocking, I don't understand how I checked that the data contents of the `MyClass` objects are actually *moved*. Maybe I am overthinking it... – Jasper Braun May 14 '20 at 17:26
  • Maybe what @MutableSideEffect meant was that I could mock `MyStruct` and test whether `MyClass`'s move constructor results in call(s) of `MyStruct`'s move constructor. I suppose that would indeed test that `MyClass`'s move constructor moves to the best of its ability, I think. – Jasper Braun May 14 '20 at 18:40

1 Answers1

0

Here is a way to track the state of an object with a Marker member variable. Keeping in mind that std::move does not mean the object will be moved from, it only makes the object eligible to be moved from.

Once an object has been moved from, its in a "valid but unspecified state" suitable for either being destructed or being re-assigned to. (A few standard C++ library types have slightly stronger guarantees regarding state after being moved from, such as std::vector and std::unique_ptr.)

#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <cassert>

#ifndef TESTING
#define TESTING 1
#endif

namespace {

struct Marker final {
    char const* state;
    ~Marker() { state = "destructed"; }
    Marker() : state{"constructed"} {}
    Marker(Marker const&) noexcept : state{"copy constructed"} {}
    Marker(Marker&& other) noexcept : state{"move constructed"} { other.state = "move constructed husk"; }
    Marker& operator=(Marker const&) noexcept { state = "assigned"; return *this; }
    Marker& operator=(Marker&& other) noexcept { state = "move assigned"; other.state = "move assigned husk"; return *this; }
    void print(std::ostream&) const;
};

void Marker::print(std::ostream& out) const {
    out << state;
}

std::ostream& operator<<(std::ostream& out, Marker const& marker) {
    marker.print(out);
    return out;
}

void test();

class BigFancyClass {
    friend void test();
    std::vector<std::string> v;
public:
    BigFancyClass() = default;
    BigFancyClass(BigFancyClass const&) = default;
    BigFancyClass(BigFancyClass&&) = default;
    BigFancyClass& operator=(BigFancyClass const&) = default;
    BigFancyClass& operator=(BigFancyClass&&) = default;

#if TESTING
    Marker mark;
#endif
};

void test() {
    std::cout << "Running test()... ";
    BigFancyClass bfc;
    bfc.v.push_back("hello");
    bfc.v.push_back("world");
    assert(bfc.v.size() == 2);
    assert(std::strcmp(bfc.mark.state, "constructed") == 0);
    BigFancyClass bfc2 = std::move(bfc);
    assert(bfc.v.size() == 0);
    assert(bfc2.v.size() == 2);
    assert(std::strcmp(bfc.mark.state, "move constructed husk") == 0);
    assert(std::strcmp(bfc2.mark.state, "move constructed") == 0);
    BigFancyClass bfc3;
    bfc3 = std::move(bfc2);
    assert(std::strcmp(bfc2.mark.state, "move assigned husk") == 0);
    assert(std::strcmp(bfc3.mark.state, "move assigned") == 0);
    std::cout << "DONE\n";
}

} // anon

int main() {
    test();
}
Eljay
  • 4,648
  • 3
  • 16
  • 27
  • When `std::move` is called on `bfc`, move constructor of `bfc.mark` is called. Does that automatically mean that move constructor of `bfc.v` is called as well? – Jasper Braun May 14 '20 at 17:36
  • If `bfc.v` has a move constructor, then it will be called. If it does not have one, then it cannot be called and instead the copy constructor will be called. If BigFancyClass has its own move constructor that isn't synthesized by the compiler, then it will have whatever behavior it is programmed to have. – Eljay May 14 '20 at 17:58
  • That means `default`ed move constructors either call move constructors for all data members which have move constructors defined or it calls none. If this statement weren't true then I don't think that this approach truly does test that `MyClass`'s data members are moved when move constructor is called. – Jasper Braun May 14 '20 at 18:36
  • The struct or class data members can have their own `Marker mark;` to track if they are being moved or not. `std::vector` and `std::unique_ptr` can check their guaranteed post-conditions to see if they're moved or not. Primitive types like `int` or `double` or raw pointers are never moved, since move is an optimization for transferring ownership of resources. – Eljay May 14 '20 at 18:47
  • This sounds a lot like mocking those data members of `MyClass` for which I would like to ensure they are actually moved, and testing whether their move constructors are called whenever `MyClass`'s move constructor is called. In this case, I would not know how to mock/add `Marker mark` to STL types that are data members of `MyClass`. – Jasper Braun May 14 '20 at 19:50
  • Yes, it does sound like a lot of mocking those data members. I don't think there is much value in checking that any member variables are moved, rather than just checking that the `MyClass` object itself is moved for the unit tests that are checking if the object was moved after whatever operation is being tested. – Eljay May 14 '20 at 19:54