تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Introducing Characterization Testing0:00

There's one very effective way to start writing tests in legacy code without touching the code itself. It's called characterization testing, for not aiming to catch bugs, but to understand and characterize the current behavior of the existing code. These tests serve as a baseline to ensure that when you modify or extend the code, you don't unintentionally change its existing behavior. Let's see how this looks in practice. We'll create characterization tests for our payment processing functionality. I created this class over here, ProcessPaymentTest, and I'll create a single method, test, and an interesting idea here introduced by Michael Feathers, who is the father of characterization.

I created this class over here, ProcessPaymentTest, and I'll create a single method, test, and an interesting idea here introduced by Michael Feathers, who is the father of characterization testing. He suggests starting with a test named X. And the purpose of this is to demonstrate that you know nothing about the expected behavior of this piece of code. And after you start exploring this behavior, you start becoming curious and digging further into additional scenarios or edge cases and trying to understand further what the code does. This is an excellent method both for documenting the existing code, but also for getting a

Writing the First Baseline Test1:16

does. This is an excellent method both for documenting the existing code, but also for getting a vast amount of understanding of the functionality. So it's an excellent next step after you've done the scratch re-vectoring. Let's start like this. I close the body of the method just to try to understand the behavior from the characterization test. So I will start by creating an instance of my Payment class. And note that characterization tests are written for legacy code where the code is so coupled together, it's difficult to test it properly using mocks or stubs.

And note that characterization tests are written for legacy code where the code is so coupled together, it's difficult to test it properly using mocks or stubs. You need to be aware of the implication it could have on the database. So prepare a testing database structure. And this exercise over here is assuming that we have that set up already in the background. Now I'm creating a new object of my Payment class, and I want to test the processPayment method. I see that it receives some variable. I still don't know what it should contain, so I will just start with assumptions. assertEquals, I'll say success, assuming that if this method is successful, it would

I still don't know what it should contain, so I will just start with assumptions. Assert equals, I'll say success, assuming that if this method is successful, it would return success message. And now I want to call array processPayment on my payment object, and I'll send it, let's say, an array with amount, because any payment should have an amount, right? So this is the purpose of characterization tests. It's supposed to be this simple because you have no further understanding of how the code works. When you start unraveling this, you will get much closer to the actual functionality of the code, and you will get an understanding that will help you write proper tests later.

Refining Inputs From Failures2:59

When you start unraveling this, you will get much closer to the actual functionality of the code, and you will get an understanding that will help you write proper tests later on. This is the reason we named our test X, and this is the reason why we are starting with assumptions. Okay, let's run our test now. Running the test named X, we are expecting success, but it returns accountNumber is required. This means that we have failed sending some parameter to our payment method over here because it can't find the accountNumber.

This means that we have failed sending some parameter to our payment method over here because it can't find the accountNumber. So let's go back, okay? So this has helped me understand a bit more about the behavior of this piece of the code, and now let's open this to see further. It receives userId, so this object should have a userId, should have an amount. It's an object, it's not an array. Let's adjust that here. So let's say amount 100, userId 1. Let's rerun the test.

So let's say amount 100, userId 1. Let's rerun the test. Okay, we got a successful transaction. The success message is different, so now I can just copy this, move it to our tests, and I can rename this to testSuccessfulTransaction. Now you can explore further scenarios from here. I know that my User with ID 1 has wire as a payment type, so let's see what else we can understand. I will open the processWirePayment. We have a bank API here, and the bank API is behaving somehow, it's hard to understand.

Characterizing Wire Payments4:43

I will open the processWire payment. We have a bank API here, and the bank API is behaving somehow, it's hard to understand. Let's see how we can work with it. So I can isolate this in a separate test, testSuccessfulWire, let's say. Again, I need an instance of the Payment class. Now I want to test this method, paymentDetails and amount. paymentDetails, I'm not really sure what the structure should be, but from here I can understand that it requires an accountNumber, accountName, and swiftCode, so let's write it like that. I will create an object, paymentDetails, and say this has accountNumber, accountName,

it like that. I will create an object, paymentDetails, and say this has accountNumber, accountName, and swift. Then my amount will be 1000, and I want to validate the response of this method. So sending the details, I will just copy this. Transaction successful. Let's see if it works. Successful wire failed, the response is not transaction successful, but accountNumber is required. Let's go back, this should be an object, not an array.

is required. Let's go back, this should be an object, not an array. Great, we are on track, it's just I missed a zero in the amount, so let's fix it. Perfect, so this is helping me get a bigger understanding of how the wire processing method works. It returns the success message that we have established over here, and now let's explore it further. So if I open the method, I can see that here we have an instance of some bank API, and what it's checking for is bank. Not really sure what this is, but it receives a swift code, so it might be validating whether

what it's checking for is bank. Not really sure what this is, but it receives a swift code, so it might be validating whether the bank is accurate. Going back to my test, I can just change this swift code, this is hypothetically a valid swift code that I have entered, so a valid format of a swift code. I can replace it with something like XXX, and see how this behaves with my tests. Execute them again, now we expected a successful transaction, we got invalid swift code. So that means that this part of the API is validating the bank depending on the provided swift code. It has helped us understand better that part of the functionality, and now I can also write

swift code. It has helped us understand better that part of the functionality, and now I can also write another test here, saying testInvalidSwiftCode, and do this. Let's try it again, great, it worked. So I'm slowly beginning to understand the code better, and I'm slowly documenting the functionality, which is even more valuable, because whoever comes after me can understand much quicker what it's all about, how it works, and all of the nuances that are present in the code. Now, what happens if instead of a simple text output, your code does something else in the background, like loading a view for example.

Snapshot Testing for Views8:18

Now, what happens if instead of a simple text output, your code does something else in the background, like loading a view for example. It's difficult to validate that with simple assertion. Luckily, there's something called snapshot testing, that is very useful in situations like this. There's a package for this, it's called phpunit-snapshot-assertions. Let's find the installation guide, copy this, require it in our project, and then let's go back to our test. I will need to include matching snapshots here, so padding, and then use the trait within our test.

I will need to include matching snapshots here, so padding, and then use the trait within our test. Now, to work with the snapshot testing, I will create another method here, so I'll just rename it to snap, and then instead of asserting a string, I'll remove this part, and I want to assert if it matches a snapshot. So what will happen with this is the first time I execute the test, it will generate a snapshot in this folder over here, and then every time I execute the test again, it will compare against the snapshot that I already have, to make sure that it all works as expected. So let's test it. It passed.

So let's test it. It passed. So I already had a snapshot of this, and it rerun the test and passed them because they are matching the existing snapshot. So everything else after this will merge the whole process with a simple string comparison. From here, you can explore all other behaviors of the code by tweaking the input values. Just be careful, the snapshots are generated for each function on the first execution, and you'll likely need to create multiple functions for all of the different scenarios that you want to explore.

that you want to explore.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟