0

I have a method that builds a query, pass it to a _make_query method in charge of resolving that query (using dns resolver) and return the answer. Then, the parent method do some stuff from the answer. I'd like to unit test the parent method ; for that I guess the best way would be to mock the _make_query method to return different outcomes and test how the parent method respond to it.

However I'm having a hard time mocking the method to return the same object returned by the dns resolver.

Here is the _make_query method:

def _make_query(self, query):

    query_resolver = resolver.Resolver()
    return query_resolver.query(query, 'SRV')

code of the calling method :

def _get_all_databases(self, database_parameters):
    query = self._format_dns_query(database_parameters)
    answers = self._make_query(query)

    databases = []

    for answer in answers:
        databases.append(
            Database(
                answer.target, answer.port, answer.weight, 
                database_parameters.db_name
        ))

    return databases

(also private as the main method get_database has then to pick a database from the list returned)

I have a mock to return what I want from this method in my unit tests, however I don't know how to reproduce the object being returned by the resolver.query() method. It should return a dns.resolver.Answer, which in turn contains a list of dns.rdtypes.IN.SRV.SRV it seems. Is there a simple way to do it?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Cholesterol
  • 177
  • 2
  • 10
  • Can I ask why you are using `__` double underscores for this method? That makes the name [class private](https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-a-single-and-a-double-underscore-before-an-object-name), i.e. protects it from accidental clashes when sub-classing. You should only use it in APIs that are intended to be sub-classed with as little friction as possible. Using it here makes mocking the method *harder*. – Martijn Pieters Aug 06 '18 at 11:26
  • Can you share the code that **uses** the results of `__make_query()`? How are those `SRV` instances used, do you call any methods on the `.name` attributes or sort those or do anything with those objects that requires specific behaviour to work? – Martijn Pieters Aug 06 '18 at 11:32
  • I want the user to only have access to a "get_database" method, so I hide the process of making query with the private method. I'll edit the main post with the method calling right now – Cholesterol Aug 06 '18 at 11:38
  • The user can access any of these methods anyway. Python does **not** have a privacy model like Java et al have. You misunderstood what the double-underscore does, here. It only makes your life as a developer a little harder, nothing more. – Martijn Pieters Aug 06 '18 at 11:41
  • The convention is to use a **single** underscore, that documents to anyone reading your code that the attribute or method is an implementation detail and should not be used from outside the API unless otherwise stated. Python assumes we are all adults and know what we are doing. – Martijn Pieters Aug 06 '18 at 11:43
  • I thought the underscore was a way of saying "don't use it, this is private". I can't enforce it, but I can at least show to the user that the usage of this method is not intended (outside of my own class). – Cholesterol Aug 06 '18 at 11:43
  • Oooh ok I understand ; thought __ meaning was private, while it's just _ – Cholesterol Aug 06 '18 at 11:45
  • `.target` is a `dns.name.Name` instance; how does `Database(...)` use that object? – Martijn Pieters Aug 06 '18 at 11:52
  • For now, it doesn't ; it just stores the data, that will be used later on. Having the name stored as a string would be completely fine I guess – Cholesterol Aug 06 '18 at 11:55
  • How does it store the data? As a string? Then `str(answer.target)` will be used (either directly or by formatting or a database adapter that uses `str(object)` to get a string value). Please be as specific as possible. – Martijn Pieters Aug 06 '18 at 11:56
  • I'm not 100% sure yet, but I guess string for target, int for port & weight is fine. Thanks for your patience btw – Cholesterol Aug 06 '18 at 11:57

1 Answers1

3

You can either mock the __make_query() method (a bit harder, since you need to manually mangle the name now to match the class-private namespace protection, see What is the meaning of single and double underscore before an object name?), or mock the Resolver() object.

You don't have to exactly match the instances produced here, you only need to produce enough of their attributes to pass muster. For the SRV class from the dnspython project, all you need is an object with port, priority, target and weight attributes, with target behaving like a dns.name.Name instance. The latter is a bit more complex, but you only need to stub out the things your code needs.

You can trivially do this with the unittest.mock library, with or without speccing out the objects precisely. For your code, all you use is 3 attributes, so your mock only ever needs to return a list with nothing more than that.

You can use the create_autospec() function to generate a mock object that's limited to the attributes the original class supports. This can help detect bugs where your code uses an attribute or method that the original classes would never allow. If you don't use a spec, then the default is to produce mock objects that allow all attributes, pretending that those attributes exist (and each such access would produce more mock objects).

So, if you need SRV instances, then I'd use:

import unittest
from unittest import mock

from dns.rdtypes.IN.SRV import SRV


def make_mock_srv(target, port, weight):
    mock_srv = mock.create_autospec(SRV)
    mock_name = mock.create_autospec(Name)
    instance = mock_srv.return_value
    instance.target = target
    instance.port = port
    instance.weight = weight
    return instance


class TestMakeQuery(unittest.TestCase):
    @mock.patch('dns.resolver.Resolver')
    def test_make_query(self, mock_resolver):
        mock_resolver_instance = mock_resolver.return_value  # the object returned by Resolver()
        mock_resolver_instance.query.return_value = [
            make_mock_srv('foo.', 1234, 2),
            make_mock_srv('bar.', 42, 4),
        ]

        # run your test, which calls _make_query, which calls Resolver().query()
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • my goal is to use this for a CNAME record. However as it is presented in your answer, I am getting an error "TypeError: 'str' object is not callable" when calling answer.target or str(answer.target). I am using python3.9 if that makes a difference – bmeyer71 Jul 17 '21 at 18:50
  • 1
    @bmeyer71 rereading my answer from 3 years ago I spotted an error; it should have set `instance.target.__str__.return_value`. I removed the Name mock entirely; unless you must have access to other Name attributes there is simply no point in mocking the `target` attribute in that much detail. – Martijn Pieters Jul 18 '21 at 10:37
  • @Martijn_Pieters thank you for the reply. I did notice that just assigning directly to .target did return what I was expecting, but I also wanted to be sure I wasn't missing anything with not mocking "Name" as well. – bmeyer71 Jul 18 '21 at 14:42