Custom Logger | Bondar Academy
Course: Playwright API Testing with TypeScript
Module: Building a Framework
Instructor: Artem Bondar
Lesson Summary
In this lesson, we created a custom logger for API tests to enhance debugging capabilities. The need for this logger arose from the limitations of the default test reports, which often lack detailed information about failed tests. Key Issues with Default Reporting Test reports only show that a test has failed without explaining why . Common status codes like 422 are displayed without context. Playwright does not log request and response details, making it hard to diagnose issues. Creating the Custom Logger We implemented a logger in a new file called logger.ts within the utils folder. The logger includes: API Logger Class : Contains a private field recentLogs to store log entries. logRequest Method : Collects details about the request, including: method (GET, POST, etc.) URL headers body (optional) logResponse Method : Captures response details, specifically statusCode and responseBody . getRecentLogs Method : Retrieves and formats the logs for easy reading. Benefits of the Custom Logger The custom logger allows us to: Aggregate request and response details for better understanding of test failures. Isolate log instances for each test, ensuring that logs from parallel tests do not interfere with each other. In summary, we developed a logger with three methods to enhance our debugging process, making it easier to analyze API test failures.
Video Transcript
All right, guys, in this lesson, we will create a custom logger for our API tests. Let me show you why do we need that. This is our smoke-test.spec.ts, and we have a test create and delete the article. Let me intentionally to fail this test and to see what our test report is going to do. I change the assertion and run the test. So running the test and test failed. So what we see in the response? So expected this, but received this. And the question, why did we get this result? Why did we received test to test? Okay, and you think, all right, let's check the report. You open the report, open in default browser. You look in the report and report does not provide much input as well. All it is showing us that the test has failed right here and we have a POST request, we have a GET request. If we expand this, we just see the line of code where the issue happened. And you're like, okay, let me rerun the test and maybe this will give me some more insights. You run the test one more time. And now, okay, now we have a new error, which is received 422. Why did that happen? So we know that 422 is a status code, but what does it mean? We go to the report, open in default browser. And I open this guy, and all I see is that received 422. And it shows where this happened, but it's not telling us why did it happen? What's the meaning of this 422 status code? So unfortunately, Playwright does not collect log information about their requests and responses when we interact with the APIs. And this information is crucial for us to understand what actually happened when the test failed. That's why we need to create a custom logger solution that will aggregate information about the request, about the response, and attach this information to the logs, okay? So let's get into it. Going back to our code and under the utils folder, I will create a new file which I called logger.ts. This is where we're gonna have our custom logger, and I create a new class. Export class, and I will call it API logger. First of all, I will create a new private field which I called recent logs of type any, and it will be an array. So we will collect all the logs as an array inside of this field. And by default, I will assign the value of the empty array as a default value. Then we create a method, log request. So this method will be responsible for collecting the request details. Which details do we want to collect? So if we go to request handler, remember before we designed the handler in a way that we collect all details independently, such as body, headers, URL, and so on. Now we can aggregate all this information inside of the logger by adding this information to the logs. About the log request, we want to know the method, which is a string. By method, I mean get, post, put, or delete, right, what kind of request it was. Then we need a URL, we want to know what we requested, which is also a string. Then we need to know the headers. And the type for the headers is this, record string and string. And the last argument that we are looking for is body, because we want to know the request body, which is, let's do type any. And we will make this parameter as optional, because sometimes we have a request body, sometimes not. For example, get and delete request do not have a request body. So this parameter will be optional. What's next? Then we will collect all those data points as part of the object. So I create a new object, which I called logEntry, for example, and put all those values in there, which is method, URL, headers, and body, that's it. And after that, we want to push this object into the recent log array right here that we created as a field inside of the class. So this recentLog.push and what we want to push. So we don't want just to push a log entry, because we want to specify what type of log entry it is. Is it a request details or it is a response details? To make sure that later when we retrieve the information from this array, we can differentiate between the JSON objects and understand, okay, what's related to request and what's related to the response. So let's add this as well. So type, this will be a request details and the data will be logEntry. Well, that's pretty much it. And let's create similar for the response, so logResponse. And for the logResponse, we care about just two things, statusCode and responseBody. So statusCode, which will be a number and responseBody, so we can remove this. For our log entry, we need just statusCode and body, we remove this. And this will be not request details, this will be a response details. All right, so method one and method two is created and we need just one last third method that will be responsible for receiving the information from the recentLogs array. getRecentLogs. It doesn't have any arguments, it's just a simple getter. So const logs and how do we get the logs? So I'm calling this recentLogs field. And I will use a method map, okay? So what is method map? Map behaves similarly like a forEach loop, for example. So map is looking into the array and taking each record from the array. And then you can do something with this record inside of the map function. So map expects a function as the argument. And we create, for example, log is the parameter and then the function inside. So log argument in this example represent a single record in the array. And it will go record by record, one by one, object by object. And then you can do with this information something, all right? So what do we need to do? Basically, we just need to print this information to the console from the data that is saved inside of this object. So I will type return and typing a backticks will build the message that I want to be printed. So first of all, let me create a header. And just to have a kind of a nice separator, I will add this equal signs. And I will print, first of all, a header for the object which will be type either requestDetails or responseDetails. So I type log.andType, this one. Log.type will print me the header for the object, either requestDetails or responseDetails. After that, on the next line, I want to print details about the object, the actual log entry. To go to the next line, I type backslash n and everything that will after that will be printed in the console from the next line. And now I want to print the details of this object. But we save the data as a JavaScript object. But we need to convert it to a JSON object, essentially to the string or to text format. To do that, we can call a JSON.Stringify method, JSON.Stringify. And this will convert the log details, JSON.Stringify, log.data into the more of string format. Or JSON format that can be printed to the console. So what else? There are three parameters. So first parameter is a value, second parameter is a replacer. We don't need to use this, so I just type null. And the third parameter, look here, is a space. So it's just a helper to make a formatting of the output data looks nicer. And I will put four to add four spaces to the right, so our object printed to the console will look nicer. Well, I hope it's not very overwhelming. I know it's kind of complex, but it is what it is. So we read the object, we print it out to the console. And if we have several objects inside of our array, we kind of want to join them together, print it as a single stack of the response. We can call a join method. And we want to separate our objects by two horizontal spaces inside of the log. And this is why I type dash n and dash n, it will add two horizontal spaces in the log. Okay, that's pretty much it. And then we want to return the logs. Yeah, that's it. So I hope it's gonna work. Let's try it, okay? So we'll go to smoketest.spec.ts, and right here, let me create just a dummy test that's not gonna do anything. We just test our logger. So test logger, just an empty function, and right here. So first of all, let's create the instance of the logger. Logger equals to new API logger. All right, and you can see the new import added from the utils file. New class is created. Logger is a new instance of this class. Now we can call the methods related to this class. Logger.logRequest, for example. Okay, first, is method. Let's say it's gonna be method get, then we need to provide URL. Let's say HTTPSTest.com slash API, something like this. What's next? Next is the headers, is the object authorization. Authorization.token, and some request body. So it's not, will be post request, okay? And it will be, let's say, foo bar, something like this. Okay, now let's log the response, log response. And here, we care about status code, let's say 200. And the second argument will be the object foo bar, okay? And now let's retrieve this information. const logs equals to logger.getRecentLogs, no arguments. It will just read the most recent details captured inside of the logger. And then console.log, and we print them logs. All right, that's it. Let's try to run it and see what's gonna happen. And yeah, look at this. It's working. So nicely printed, separated request details and response details. So this is the header I mentioned before, which is this guy, request details. It's printed this way, and then we have log entry information. So everything would be added as arguments to this method. Method post, URL is this one, headers and body. And then response details. Status code was 200, and response body was this one. So now this information, knowing this, we can easily debug our test and understand what type of information and how our API made the request. And what came back as a response. By looking into that, you can analyze what went wrong, okay? And let me show you one more important thing about object-oriented programming. So here we created the instance of API logger, right? But I can create a second instance of the same logger and call it, for example, logger number two. Now logger number one and number two will be completely parallel, isolated instances of our logger class. And information collected inside of each instance is independent and test will not interfere when we use a separate instance of a logger for every test, so let me demonstrate that. So I copy this and paste this, and let's say for the logger two, it will be, let's say, getRequest and API, I don't know, 123, for example. Then we retrieve information for the second logger, logger2.getRecentLogs, logs2 as well. And let's print this out to the console. Logs2 as well, run it, and look at this. Now we have two requests and response, and they are completely separated. So this is the logger number one, this is the logger number two. So this is a very cool feature of object-oriented programming when each instance of the class is completely isolated. So if we create a new instance of a logger for every test independently, then even if we run the tests in parallel, we still have isolated aggregation of the logs per test. So this is very, very cool. All right, so let's quickly summarize what we did in this lesson. So we created a custom logger with just three methods, toLogRequest, logResponse, and getRecentLog. logRequest aggregates four details about the request, method, URL, headers, and body, and logResponse collects just status code and body. All this information is added into the recent logs array. And then we can use getRecentLogs method to loop through this array and return a single list of the entire log trace in the chronological order of request and response details. And we can use this information to attach, for example, to reporter to make the debugging of the failed test much easier. All right, so let's do it in next lesson.