Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out the Harness changes for test_model.py #196

Closed

Conversation

jameinel
Copy link
Member

If you want to call something like Model.relations['db'] when there
isn't a relation established, we need to have CharmMeta in the
ModelBackend so we give the right errors.

Also, track all the calls to the model backend, so when doing Model tests, we can validate that we interact with the backend in the way we expect.

If you want to call something like Model.relations['db'] when there
isn't a relation established, we need to have CharmMeta in the
ModelBackend so we give the right errors.

Also, track all the calls to the model backend, so we can assert them.
@jameinel jameinel mentioned this pull request Mar 31, 2020
Copy link
Contributor

@facundobatista facundobatista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Added some comments inline, thanks!

ops/testing.py Outdated Show resolved Hide resolved
ops/testing.py Show resolved Hide resolved
ops/testing.py Outdated Show resolved Hide resolved
ops/testing.py Outdated Show resolved Hide resolved
ops/testing.py Outdated Show resolved Hide resolved
dshcherb pushed a commit to dshcherb/cockroachdb-operator-1 that referenced this pull request Apr 1, 2020
This PR needs to go in
canonical/operator#196

to avoid

canonical/operator#202

when running tests.
ops/testing.py Outdated
@@ -319,16 +319,33 @@ def set_leader(self, is_leader=True):
if is_leader and not was_leader and self._charm is not None and self._hooks_enabled:
self._charm.on.leader_elected.emit()

def get_backend_calls(self, reset=False):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While here, I went ahead and changed the default to "reset=True'. Having written a few tests, it is usually quite a bit clearer to just list the calls since the last time you checked, rather than having a growing list that says [('a',)] then [('a',),('b',)], then ([('a',), ('b',), ('c'),)]

best to change it before anyone depends on it.

Use a record_calls decorator that creates the same output that we've
been using, but does so without having to change each method, or ever
forget about other args, etc.
Copy link
Collaborator

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a first pass:

ops/testing.py Outdated
@@ -173,7 +173,7 @@ def add_relation(self, relation_name, remote_app, remote_app_data=None):
# remote_app_data isn't empty.
return rel_id

def add_relation_unit(self, relation_id, remote_unit_name, remote_unit_data=None):
def add_relation_unit(self, relation_id, remote_unit_name, *, remote_unit_data=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to discuss the idea of these extra parameters, per discussion on that other PR, sooner rather than later, otherwise people will start trusting the parameters and we will drive people mad by breaking their tests.

@@ -319,16 +319,60 @@ def set_leader(self, is_leader=True):
if is_leader and not was_leader and self._charm is not None and self._hooks_enabled:
self._charm.on.leader_elected.emit()

def get_backend_calls(self, reset=True):
"""Return the calls that we have made to the TestingModelBackend.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit premature to stick that in our exposed API. We should discourage the idea of testing by checking what exact methods the implementation has called, and should instead encourage testing by checking that the expected behavior did take place. This is strongly encouraging the former.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was because I wanted to use Harness for internal testing, but I agree it shouldn't be part of the exposed-to-charmers interface.



def record_calls(cls):
"""Replace methods on cls with methods that record that they have been called.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea.

ops/testing.py Outdated
class _TestingModelBackend:
"""This conforms to the interface for ModelBackend but provides canned data.

You should not use this class directly, it is used by `TestingHarness`_ to drive the model.
You should not use this class directly, it is used by `Harness`_ to drive the model.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/You should not/Do NOT/

ops/testing.py Outdated
return self._relation_ids_map[relation_name]
except KeyError as e:
if relation_name not in self._meta.relations:
raise model.ModelError('{} is not a known relation'.format(relation_name)) from e
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

ops/testing.py Outdated
try:
return self._relation_list_map[relation_id]
except KeyError as e:
raise model.RelationNotFoundError from e
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, nice to see the model being tuned to reflect the real implementation. I suspect we'll have a lot of that in the near future.

@@ -49,6 +51,25 @@ def test_add_relation(self):
self.assertEqual([rel_id], backend.relation_ids('db'))
self.assertEqual([], backend.relation_list(rel_id))

def test_add_relation_must_name_app_data(self):
# language=YAML
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange.. what are those about? They don't feel so great here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They cause my editor to highlight the string text as YAML rather than just a string, but we can get rid of them. I'll clean them up, as they aren't docs for the tests, which is what we're currently doing.

harness.set_leader(False)
rel_id = harness.add_relation('db', 'postgresql')
harness.update_relation_data(rel_id, 'test-charm/0', {'foo': 'bar'})
harness.add_relation_unit(rel_id, 'postgresql/0')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those interactions via the harness are great! So readable and so clear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that Harness probably shouldn't use get_backend_calls for Charm testing. For test_model.py something like this is very useful, because we're explicitly testing how our Model object interacts with our ModelBackend. (currently we do so via a fake_script indirection).

I'd happily make get_backend_calls private for our internal use, and not leave it as part of the public API.

self.assertEqual([
('relation_ids', 'db'),
('relation_list', rel_id),
], harness.get_backend_calls(reset=True))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the awkward bit.. what just happened there? How and why do we constrain the above logic to that list? Why does it matter?

Make get_backend_calls a private method, as it is useful for the
TestModel tests, but should not be used by charmers interacting with
Harness.
Copy link
Contributor

@facundobatista facundobatista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all green now. Thanks!!!

dshcherb pushed a commit to dshcherb/cockroachdb-operator-1 that referenced this pull request Apr 8, 2020
* moved the code that mutates the system state to a separate type
  (DbInstanceManager);
* moved events previously emitted by the charm to DbInstanceManager
  * this created a need for the charm code to subscribe the
    CockroachDbCluster type to the ClusterInitialized event from
    CockroachDbCharm and a subsequent change to how this event is unit
    tested in test_cluster.py;
* dependency injection of TestDbInstanceManager is done via a class
  attribute which is far from ideal but given the charm type is
  initialized by the framework there is no direct way to control its
  arguments;
* TestDbInstanceManager overrides the methods that modify the system
  state leaving the rest of the logic interesting for testing intact;

3 test cases need uncommenting either partially or fully when PR 196
  gets merged:
canonical/operator#196
dshcherb pushed a commit to dshcherb/oper-interface-ceph-client that referenced this pull request Apr 13, 2020
* some unit tests use mocking because the interface code calls out to
  charm-helpers which uses hookenv. In order to avoid that we need to
  solve a larger problem of depending on charm-helpers and bypassing of
  the model and test harness provided by the framework.
* there are 2 expected failures which are waiting on
  canonical/operator#196 to be merged.
Cleanup the API for the new expectations (using update_relation_data
instead of a remote_app_data parameter).
@gnuoy
Copy link
Contributor

gnuoy commented Apr 24, 2020

I have tested this PR as a potential fix for issue #241 and it does indeed fix it.

@jameinel jameinel mentioned this pull request May 5, 2020
jameinel added a commit that referenced this pull request May 6, 2020
This is #196 but without the _get_backend_calls.
I'll probably bring that back later on, but for now this fixes a few key aspects of Harness. Namely:

 *  fixes: #241
 *  We should not be raising KeyError for a relation that isn't established yet. It is normal for a charm to want to see if something is ready.
 *  Allow relation_set to actually remove values (when set to '') which conforms to Juju's behavior
    remove language=YAML comments (they were considered noisy)
One 'advantage' of squash-and-merge is that it doesn't automatically roll
back when you override a change. (Sequential branches are going to be a
pain.)

This leaves the _record_calls and _get_backend_calls around, prepared
for when we use them in test_model.py
@jameinel
Copy link
Member Author

jameinel commented May 6, 2020

Closing this for now, as it isn't making use of the _get_backend_calls (which are intended for test_model.py). I'll bring it back once that makes it clear how this helps.

@jameinel jameinel closed this May 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants