this post was submitted on 04 Jul 2024
11 points (100.0% liked)

Python

6368 readers
7 users here now

Welcome to the Python community on the programming.dev Lemmy instance!

πŸ“… Events

PastNovember 2023

October 2023

July 2023

August 2023

September 2023

🐍 Python project:
πŸ’“ Python Community:
✨ Python Ecosystem:
🌌 Fediverse
Communities
Projects
Feeds

founded 1 year ago
MODERATORS
 

I have a repository that contains multiple programs:

.
└── Programs
 Β Β  β”œβ”€β”€ program1
 Β Β  β”‚Β Β  └── Generic_named.py
 Β Β  └── program2
 Β Β      └── Generic_named.py

I would like to add testing to this repository.

I have attempted to do it like this:

.
β”œβ”€β”€ Programs
β”‚Β Β  β”œβ”€β”€ program1
β”‚Β Β  β”‚Β Β  └── Generic_named.py
β”‚Β Β  └── program2
β”‚Β Β      └── Generic_named.py
└── Tests
    β”œβ”€β”€ mock
    β”‚Β Β  β”œβ”€β”€ 1
    β”‚Β Β  β”‚Β Β  └── custom_module.py
    β”‚Β Β  └── 2
    β”‚Β Β      └── custom_module.py
    β”œβ”€β”€ temp
    β”œβ”€β”€ test1.py
    └── test2.py

Where temp is a folder to store each program temporarily with mock versions of any required imports that can not be stored directly with the program.

Suppose we use a hello world example like this:

cat Programs/program1/Generic_named.py
import custom_module

def main():
    return custom_module.out()


cat Programs/program2/Generic_named.py
import custom_module

def main():
    return custom_module.out("Goodbye, World!")


cat Tests/mock/1/custom_module.py
def out():return "Hello, World!"


cat Tests/mock/2/custom_module.py
def out(x):return x

And I were to use these scripts to test it:

cat Tests/test1.py
import unittest
import os
import sys
import shutil

if os.path.exists('Tests/temp/1'):
    shutil.rmtree('Tests/temp/1')

shutil.copytree('Tests/mock/1', 'Tests/temp/1/')
shutil.copyfile('Programs/program1/Generic_named.py', 'Tests/temp/1/Generic_named.py')

sys.path.append('Tests/temp/1')
import Generic_named
sys.path.remove('Tests/temp/1')

class Test(unittest.TestCase):
    def test_case1(self):
            self.assertEqual(Generic_named.main(), "Hello, World!")

if __name__ == '__main__':
    unittest.main()



cat Tests/test2.py
import unittest
import os
import sys
import shutil

if os.path.exists('Tests/temp/2'):
    shutil.rmtree('Tests/temp/2')

shutil.copytree('Tests/mock/2', 'Tests/temp/2')
shutil.copyfile('Programs/program2/Generic_named.py', 'Tests/temp/2/Generic_named.py')

sys.path.append('Tests/temp/2')
import Generic_named
sys.path.remove('Tests/temp/2')

class Test(unittest.TestCase):
    def test_case1(self):
            self.assertEqual(Generic_named.main(), "Goodbye, World!")

if __name__ == '__main__':
    unittest.main()

Both tests pass when run individually:

python3 -m unittest Tests/test1.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK


python3 -m unittest Tests/test2.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

However, they fail when being run together:

python3 -m unittest discover -p test*.py -s Tests/
.F
======================================================================
FAIL: test_case1 (test2.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/s/Documents/Coding practice/2024/Test Mess/1/Tests/test2.py", line 18, in test_case1
    self.assertEqual(Generic_named.main(), "Goodbye, World!")
AssertionError: 'Hello, World!' != 'Goodbye, World!'
- Hello, World!
+ Goodbye, World!


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

If I try to use a different temporary name for one of the scripts I am trying to test,

cat Tests/test2.py
import unittest
import os
import sys
import shutil

if os.path.exists('Tests/temp/2'):
    shutil.rmtree('Tests/temp/2')

shutil.copytree('Tests/mock/2', 'Tests/temp/2')
shutil.copyfile('Programs/program2/Generic_named.py', 'Tests/temp/2/Generic_named1.py')

sys.path.append('Tests/temp/2')
import Generic_named1
sys.path.remove('Tests/temp/2')

class Test(unittest.TestCase):
    def test_case1(self):
            self.assertEqual(Generic_named1.main(), "Goodbye, World!")

if __name__ == '__main__':
    unittest.main()

Then I get a different error:

python3 -m unittest discover -p test*.py -s Tests/
.E
======================================================================
ERROR: test_case1 (test2.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/s/Documents/Coding practice/2024/Test Mess/2/Tests/test2.py", line 18, in test_case1
    self.assertEqual(Generic_named1.main(), "Goodbye, World!")
  File "/home/s/Documents/Coding practice/2024/Test Mess/2/Tests/temp/2/Generic_named1.py", line 4, in main
    return custom_module.out("Goodbye, World!")
TypeError: out() takes 0 positional arguments but 1 was given

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (errors=1)

It seems to be trying to import the same file, despite me using a different file from a different path with the same name. This seems strange, as I've been making sure to undo any changes to the Python Path after importing what I wish to test. Is there any way to mock the path? I can't change the name of the custom_module, as that would require changing the programs I wish to test.

How should I write, approach, or setup these tests such that they can be tested with unittest discover the same as they can individually?

top 4 comments
sorted by: hot top controversial new old
[–] sajran@lemmy.ml 4 points 4 months ago

Since you have all your shutil.copytrees and sys.path manipulation at the top level of the test modules, they are executed the moment those modules are imported. unittest likely imports all discovered test modules before actually executing the tests so the set up of both modules is executed in random order before the tests are run. The correct way to perform test setup is using setUp and setUpClass methods of unittest.TestCase. Their counterparts tearDown and tearDownClass are used to clean up after tests. You probably will be able to get this to work somehow using those methods.

However, I'm fairly certain that this entire question is an example of the XY problem and you should be approaching this whole thing differently. Copying the modules and their mock dependencies into a temporary directory and manipulating sys.path seems like an absolute nightmare and it will be a massive PITA even if you get it to a working state. I don't know what problem exactly you're trying to solve but I think you should really read up on unittest.mock and even more importantly on dependency injection.

[–] onlinepersona@programming.dev 3 points 4 months ago

I have to admit, I don't get what the copying is doing. It seems like you're trying to recreate some scenario. Just in case, do you know of the import ... as ... expression? (doc with examples). Maybe that'll help you.

Anti Commercial-AI license

[–] gedhrel@lemmy.world 2 points 4 months ago* (last edited 4 months ago)

The "why" is that the import system is caching modules in sys.modules.

The "what to do about it" is "not this". Use a package layout with explicit names (p1.generic_name etc) instead.

You can use relative imports under those packages if you prefer (from .generic_name import ...).

If you want executables on the path look at setup.py or any of the myriad of overlapping modern equivalents that'll let you specify a command-libe executable to install, then pip install -e . to install it in your venv's bin dir.

[–] anzo@programming.dev 1 points 4 months ago

There were times during my programming career that I just started the whole work from scratch, rethinking from the top (where I want to get) to bottom (how I have been doing it). I am sorry that I cant help you anymore, but please give this a try. Do not remove what you have, just ignore it for a moment (a day?) and start over. You might hit another wall, or no. Who knows? Good luck!