0

I am running tests using python, selenium and pytest framework with page object models.

As of now, I moved my logger method from BaseClass to conftest.py because, if I wanted to log something, I had to create a log object in each test case.

Reproducible Test case as follows:

from baseclass import BaseClass

class TestClass(BaseClass):

    def test_case1(self):
        self.log.info('testing log!')

    def test_case2(self):
        self.log.info('second log!')

I abstracted logger method into conftest.py, but when I ran the test, I got the file name in logs as setup instead of test case name. Based on other suggestions(thank you SO community), I passed the name of the file as an arg to logger() method but now it prints every log with the first test case's name only.

# Using logger(): name = inspect.stack()[0][3]
> 2021-01-27 05:42:42,377 : INFO : setup : testing log!

# Using logger(full_name), where full_name = os.environ.get('PYTEST_CURRENT_TEST').split(' ')[0]
# Similar results when using full_name = request.node.__name__ or request.module.__name__
> 2021-01-28 02:12:39,877 : INFO : test_my_page.py::TestClass::test_case1 : testing log!
> 2021-01-28 02:12:39,877 : INFO : test_my_page.py::TestClass::test_case1 : second log!

My conftest.py is setup as follows:

import pytest
from selenium import webdriver
import inspect
import logging

def logger():
    log_item = logging.getLogger(name)
    with open('LogFile.log', 'w+') as logFile:
        logFile.seek(0)
        logFile.truncate()
    file_handler = logging.FileHandler("LogFile.log")
    log_format = logging.Formatter("%(asctime)s : %(levelname)s : %(name)s : %(message)s")
    file_handler.setFormatter(log_format)
    log_item.addHandler(file_handler)
    log_item.setLevel(logging.DEBUG)
    return log_item


@pytest.fixture(scope='class')
def setup(request):

    driver = webdriver.Chrome(executable_path=TestData.CHROME)
    full_name = os.environ.get('PYTEST_CURRENT_TEST').split(' ')[0]

    request.cls.log = logger(full_name)
    request.cls.driver = driver

    yield
    driver.close()

I actually want to get the name of the tests being performed. Like below:

2021-01-27 01:57:02,594 : INFO : test_my_page.py::TestClass::test_case1 : testing log! 2021-01-27 01:57:32,292 : INFO : test_my_page.py::TestClass::test_case2 : second log!

How can I get the logger method to print the name of the test cases dynamically without moving the method back to BaseClass? I am about to write a lot of test cases, and trying to find a better way as it's repetitive to create a logger object in each test case.

My BaseClass is as below:

import pytest

@pytest.mark.usefixtures('setup')
class BaseClass:
    pass
Cres
  • 90
  • 8
  • 1
    Does this answer your question? [py.test: how to get the current test's name from the setup method?](https://stackoverflow.com/questions/17726954/py-test-how-to-get-the-current-tests-name-from-the-setup-method) – MrBean Bremen Jan 27 '21 at 11:39
  • @MrBeanBremen Thank you for suggesting a similar question, but this does not solve the problem I have with same test name appearing in all the logs. As I understand, conftest.py > setup() is run only once at the start of the test, and getting the test case names dynamically has been a challenge. – Cres Jan 27 '21 at 20:40
  • Right, your `setup` has class scope, so you won't get the name from the request - I had missed that, sorry. But there is also an answer to use the environment variable (as also shown in the answer below), so it might still have helped you. – MrBean Bremen Jan 27 '21 at 20:49
  • @MrBeanBremen I tried using the Pytest env variable as mentioned below, but still have this problem. I see a probable solution on the other question you suggested, extracting all names from dir: test_names = [n for n in dir(self) if n.startswith('test_')] But, I don't think that's the best solution to this yet. – Cres Jan 27 '21 at 21:15

1 Answers1

1

Here's my solution:

File temp_test.py: using your class name will make pytest not recognizing the test case, so I renamed it as TestClass. Your function name is still test_case1. Read more about pytest naming convention.

from baseclass import BaseClass

class TestClass(BaseClass):

    def test_case1(self):
        # log = self.logger()
        # log.info('testing log!') # old method

        self.log.info('testing log 1!') # new method

I added another test file z_test.py:

from baseclass import BaseClass

class Test2(BaseClass):

    def test_case2(self):
        # log = self.logger()
        # log.info('testing log!') # old method

        self.log.info('testing log 2!') # new method

    def test_case3(self):
        # log = self.logger()
        # log.info('testing log!') # old method

        self.log.info('testing log 3!') # new method

File: conftest.py

import os
import pytest
import logging


def logger(name):
    log_item = logging.getLogger(name)
    with open('LogFile.log', 'a') as logFile:
        logFile.seek(0)
        # logFile.truncate()
    file_handler = logging.FileHandler("LogFile.log")
    log_format = logging.Formatter("%(asctime)s : %(levelname)s : %(name)s : %(message)s")
    file_handler.setFormatter(log_format)
    log_item.addHandler(file_handler)
    log_item.setLevel(logging.DEBUG)
    return log_item

@pytest.fixture(scope='function')
def setup(request):
    # full name: 'class name::function name'
    full_name = os.environ.get('PYTEST_CURRENT_TEST').split(' ')[0]
    # full_name = request.module.__name__ # print the function name
    # full_name = request.node.name # print the class name
    request.cls.log = logger(full_name)
    yield
    print('close')

In conftest.py, I modified some:

  • logger function will now have name argument. Your test setup will provide this name.
  • os.environ.get('PYTEST_CURRENT_TEST').split(' ')[0] will provide the setup full name, in this case the output will be 2021-01-27 14:00:23,436 : INFO : temp_test.py::TestClass::test_case1 : testing log!
  • full_name = request.module.__name__ will provide the function name only: test_case1
  • full_name = request.node.name will provide the class name only: TestClass.

I think it's best to use PYTEST_CURRENT_TEST env var, and do string manipulation with it.


Update for your comment on not seeing other test's name generated:

  • setup() needs to be at function scope, because for your each test, you will want to put your test into functions. If you use it as class as before, it will only call the setup once, at the start of the first test function. In my example, it will only output test_case2 twice.
  • open('LogFile.log', 'w+') should be open('LogFile.log', 'a'). Mode a will append the text, mode w will just delete everything, so only your last test case's log will be saved.
  • logFile.truncate() will also delete everything except for the last test case's log.

Output I have:

2021-01-28 15:22:58,540 : INFO : temp_test.py::TestClass::test_case1 : testing log 1!
2021-01-28 15:22:58,543 : INFO : z_test.py::Test2::test_case2 : testing log 2!
2021-01-28 15:22:58,545 : INFO : z_test.py::Test2::test_case3 : testing log 3!

Play around with these lines, you will get what you need.

Another tip: when running test, you will want to create a separate report folder, containing log files, name it randomly with dates, hours, minutes...etc. Using a generic name like LogFile.log will cause the text to append, maybe LogFile_2021_01_28_152101.log is better, your choice.

jackblk
  • 1,076
  • 8
  • 19
  • Thank you for your response. I tried using the Pytest env var as you suggested, and now I see the same test case name in every log, instead of having the name of the test case sourced dynamically. request.method didn't work. Neither did moving the logger method into setup() itself, as it has (scope='class') Any way to work around this? – Cres Jan 27 '21 at 21:19
  • I updated the answer with more test code example & output. The problem is with the scope of the `setup()`, truncate & write mode. You will need to play around the example code to fit your best. Please accept the answer if it helps you :). – jackblk Jan 28 '21 at 08:26
  • Thanks @jackblk Can't set the scope of setup() to function. I do not want the browser to close after every test coz of teardown method. I haven't provided the entire code, and only gave the necessary blocks in my examples. Hence, the 'w+' method in logger(), and setup(scope='class'). Also, the log file name is also kept short here for simplicity purposes and I've removed the reports dir in this example. I've upvoted your answer coz it's helpful. – Cres Jan 28 '21 at 21:29
  • 1
    Ah, so you're using Selenium to test right? In that case, there's another way for you to achieve what you want. Your setup should just do the browser setup, don't attach your logger in setup(). Move your `request.cls.log = logger(full_name)` to another function like function_logger(). Decorate it with `@pytest.fixture(scope='function', autouse=True)` to make it auto use in every function. This will work for you :). – jackblk Jan 29 '21 at 07:23
  • This works perfectly. Had to made some modifications and documentation helped there. Thank you! ^_^ – Cres Jan 30 '21 at 17:35