Examples: query, "exact match", wildcard*, wild?ard, wild*rd
Fuzzy search: cake~ (finds cakes, bake)
Term boost: "red velvet"^4, chocolate^2
Field grouping: tags:(+work -"fun-stuff")
Escape special characters +-&|!(){}[]^"~*?:\ - e.g. \+ \* \!
Range search: properties.timestamp:[1587729413488 TO *] (inclusive), properties.title:{A TO Z}(excluding A and Z)
Combinations: chocolate AND vanilla, chocolate OR vanilla, (chocolate OR vanilla) NOT "vanilla pudding"
Field search: properties.title:"The Title" AND text
 
Answered
How can unit tests be written so that the "ajax" decorator does not interfere with testing?

In writing extensions, methods that are decorated with the ajax method frequently appear. The code below is an example.

from q2_sdk.core.web import ajax
from q2_sdk.models.tecton import TectonResponse, Success

@ajax
async def func_to_be_tested(self) -> TectonResponse:
	# Do some things
	return Success()

For writing unit tests for functions such as these, we do not want to execute logic contained in the ajax decorator. This interfers with tests and muddies the results by calling on logic not contained in the method to be tested. I have attempted to mock the ajax decorator in unit tests to stop its logic from executing, but this is not working.

import pytest
import logging
import asyncio
from functools import wraps
from unittest.mock import Mock, AsyncMock, patch, PropertyMock
from q2_sdk.ui.forms import Q2Form

# This decorator has no effect. It can be used to patch decorators so that the logic of a decorated
# method can be tested without interference.
def mock_decorator(*args, **kwargs):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)

        return decorated_function

    return decorator
		
# The ajax decorator has been patched by the mock_decorator.
patch("MyExtension.extension.ajax", mock_decorator).start()

from tornado.web import Application
from q2_sdk.tools.testing.models import RequestMock
from q2_sdk.tools.testing.mock_handler_mixin import MockHandlerMixin
from .. import extension

class MockMyExtensionHandler(
    extension.MyExtensionHandler, MockHandlerMixin
):
    def __init__(self):
        super().__init__(Application(), RequestMock(), logging_level="INFO")
        self.mock_online_user()
        self.mock_online_session()
				
@patch("MyExtension.extension.Success")
async def test_func_to_be_tested(mock_success_class):
    handler = MockMyExtensionHandler()
   
	 # The Success class is patched so that it can be controlled by our unit test.
		mock_success = mock_success_class.return_value
    mock_success.data = None
    mock_success.status = 200

    actual = await handler.func_to_be_tested()

    assert isinstance(actual, type(mock_success))

The above code attempts to patch the the ajax decorator with a decorator that has no effect on the code. This new decorator, mock_decorator , is useful for testing the logic of decorated methods without executing the logic contained in decorators. When this code is executed, however, an error message appears that reads, "q2_sdk.core.exceptions.InvalidReturnType: Ajax target method must return a dictionary or TectonResponse". This error is being triggered by the ajax decorator, so the patch failed. So long as the ajax decorator cannot be patched or somehow disabled, func_to_be_tested cannot be properly tested.

For methods that use the ajax decorator, how would you recommend writing unit tests for them?

Posted 2 years ago
Votes Newest

Answers


Hi Ethan,

You have the right idea with mocking the decorator. In order for it to work, you could use monkeypatch to override the decorator with its mock and use importlib.reload to ensure that the changes are reflected (module caching appears to be the reason for which your mock isn't applying as expected.

E.g
You have a function that substracts 2 from a provided value, and a decorator defined in mydecorator.py that adds 4 to the value:

from mydecorator import add_four_decorator

@add_four_decorator
def subtract(a):
    return a - 2

Mocking add_four_decorator in the unit test would look like the following:

import calculator

def do_nothing(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
    
def test_calculator_subtract(monkeypatch):
    monkeypatch.setattr('mydecorator.add_four_decorator', do_nothing)
    import importlib
    importlib.reload(calculator)
    res = calculator.subtract(12)
    assert res == 10

Above we are grabbing the monkeypatch fixture and using monkeypatch.setattr to pass in the <file_path>.<function_name> of the decorator, and the decorator to replace it with as the second parameter. We are then using importlib.relead to import the module again (rather than using the cached file) to ensure the mock is applied.

The same idea would apply with ajax here and your test_func_to_be_tested would contain something like this:


from q2_sdk.core import web
async def test_func_to_be_tested(mock_success_class):
...
monkeypatch.setattr('q2_sdk.core.web.ajax', mock_decorator)
import importlib
importlib.reload(web)
actual = await handler.func_to_be_tested()
assert isinstance(actual, type(mock_success))
0

@<1455287096196468736|Solene Sossah> Hi, Solene. Thank you for the idea of reloading the module. In the last example that you gave, you wrote that calculator is the module that should be reloaded. In the context of a unit test for the SDK extension, which module is it that actually needs to be reloaded? When I attempt to do it on "extension", it only clears out all of my other patches.

— Ethan Pittman   2 years ago
0

Hi Ethan, sorry there was a typo in the provided code. I believe you would want to reload the file that contains ajax (web.py in this case from q2_sdk.core). I have updated the example in my previous response

— Solene Sossah   2 years ago
0

@<1455287096196468736|Solene Sossah> For some reason, this is still having no effect. It is as though the decorator is not being patched at all. For testing purposes, it may be the most reliable thing at this point to just let the decorator run and ensure that returning a TectonResponse object returns the expected output. It's not ideal and really isn't a unit test, but it still provides some degree of test coverage.

— Ethan Pittman   2 years ago
105 Views
1 Answer
2 years ago
2 years ago