1

I'm trying to test a tool I'm building which uses some jMetalPy functionality. I had/have a previous version working but I am now trying to refactor out some external dependencies (such as the aforementioned jMetalPy).

Project Code & Structure

Here is a minimalist structure of my project.

MyToolDirectory
  ¦--/MyTool
  ¦----/__init__.py
  ¦----/_jmetal
  ¦------/__init__.py
  ¦------/core
  ¦--------/quality_indicator.py
  ¦----/core
  ¦------/__init__.py
  ¦------/run_manager.py
  ¦----/tests
  ¦------/__init__.py
  ¦------/test_run_manager.py

The _jmetal directory is to remove external dependency on the jMetalPy package - and I have copied only the necessary packages/modules that I need.

Minimal contents of run_manager.py

# MyTool\core\run_manager.py

import jmetal
# from jmetal.core.quality_indicators import HyperVolume  # old working version

class RunManager:
    def __init__(self):
      pass

    @staticmethod
    def calculate_hypervolume(front, ref_point):
        if front is None or len(front) < 1:
            return 0.
        hv = jmetal.core.quality_indicator.HyperVolume(ref_point)
        # hv = HyperVolume(ref_point)
        hypervolume = hv.compute(front)
        return hypervolume

Minimal contents of test_run_manager.py

# MyTool\tests\test_run_manager.py
import unittest
from unittest.mock import MagicMock, Mock, patch

from MyTool import core

class RunManagerTest(unittest.TestCase):
    def setUp(self):
        self.rm = core.RunManager()

    def test_calculate_hypervolume(self):
        ref_points = [0.0, 57.5]
        front = [None, None]
        # with patch('MyTool.core.run_manager.HyperVolume') as mock_HV:  # old working version
        with patch('MyTool.core.run_manager.jmetal.core.quality_indicator.HyperVolume') as mock_HV:
            mock_HV.return_value = MagicMock()
            res = self.rm.calculate_hypervolume(front, ref_points)
            mock_HV.assert_called_with(ref_points)
            mock_HV().compute.assert_called_with(front)

Main Question

When I run a test with the code as-is, I get this error message:

E           ModuleNotFoundError: No module named 'MyTool.core.run_manager.jmetal'; 'MyTool.core.run_manager' is not a package

But when I change it to:

        with patch('MyTool.core.run_manager.jmetal.core') as mock_core:
            mock_HV = mock_core.quality_indicator.HyperVolume
            mock_HV.return_value = MagicMock()
            res = self.rm.calculate_hypervolume(front, ref_points)
            mock_HV.assert_called_with(ref_points)
            mock_HV().compute.assert_called_with(front)

... now the test passes. What gives?!

Why can't (or rather, how can) I surgically patch the exact class I want (i.e., HyperVolume) without patching out an entire sub-package as well? Is there a way around this? There may be code in jmetal.core that needs to run normally.

Is the reason this isn't working only because there is no from . import quality_indicator statement in jMetalPy's jmetal\core\__init__.py ? Because even with patch('MyTool.core.run_manager.jmetal.core.quality_indicator) throws:

E           AttributeError: <module 'jmetal.core' from 'path\\to\\venv\\lib\\site-packages\\jmetal\\core\\__init__.py'> does not have the attribute 'quality_indicator'

Or is there something I'm doing wrong?

In the case that it is just about adding those import statements, I could do that in my _jmetal sub-package, but I was hoping to let the user default to their own jMetalPy installation if they already had one by adding this to MyTool\__init__.py:

try:
    import jmetal
except ModuleNotFoundError:
    from . import _jmetal as jmetal

and then replacing all instances of import jmetal with from MyTool import jmetal. However, I'd run into the same problem all over again.

I feel that there is some core concept I am not grasping. Thanks for the help.

Folarin
  • 11
  • 1
  • 2

0 Answers0