How can we change our behaviour to write tests before writing any code


I remember when I wanted to write my first test, I was very nervous and I did not know what should I do and from where should I start. I was totally terrified because I was thinking of losing time and not accomplishing the whole feature. And I think it was very normal behavior of my self.

because now when I think about it, writing tests for the first time it's very horrifying and actually you don't know if you are in the right way because it's your first time. so I have accomplished my first test case and I started to work on the other ones. and after some time when I get back to my code I was shocked because I was using a very old way to write tests and it was a very hard time to just write one test, I understand my self at that moment. but after that, I get a little bit better.

 

Now here is my recommendation of how to start writing tests before writing any code. 

Before anything, keep in mind that writing tests are a culture. But what do I mean with culture? it's culture because if the company that you are working in it somehow does not gives value to writing tests or not accepting writing tests of the whole process of accomplishing a feature, then you'll face a problem. so this is the first thing that we should think about it, it's doable in that environment or not. I think most of the companies these days are agree on writing tests for their software unless they are building some kind of software which they cant charge the customers for writing tests for their products. also for that kind of software, I think it's super needed. I just wanted to say that it's highly dependent on the environment.

 

When a new sprint starts out, you'll see your tasks and you'll start with the first one or the high priority one. you'll select the task to move it to in-progress status and prepare your branch and start working on it. before anything, we should know, what is the goal of this task. let's assume we were given a task with the following title, Make an endpoint to charge a parent for a specific amount. (User should be able to add money to his/her wallet)

Let's assume that the parent has already registered his card information and we have them stored in the database.

Keep in mind we are using strip payment gateway to charge the users.

Now let's think about this task for a min. What we are trying to achieve in this task?

  • We are trying to charge the user for a specific amount.
  • I'll explain it like this, the user tries to add money to his/her wallet so he/she can use that money to do sth with it in the future.

How we can achieve that?

  1. We need an API with the name of api/user/charge
  2. We need to accept a request with specific fields

Now we are going to write our first test scenario, we can start with the functional tests. The first scenario could be writing tests to validate the request 

Okay, that's a good scenario. The request should contain two fields, the first one should be the amount and the second one should be the description. Like the following code.

{
    "amount": 1000,
    "description": "any description"
}

Let's think about it for a min and let's write the scenarios for this validation

  • The amount should be an integer  
  • The amount should not be less than 1
  • The amount is required
  • The amount should not be more than an "X" amount
  • The description should be in string format
  • The description is required
  • The description can't be more than "X" character

As you can see we have written all of the scenarios regarding the validation part.

Let's start with our first test

class UserTest extends TestCase
{
    protected const HTTP_FORBIDDEN = 422;

    /**
     * @test
     * A basic test example.
     *
     * @return void
     */
    public function charge_should_throw_a_validation_error_if_amount_was_less_than_fifty()
    {
        $this->postJson('api/user/charge', ['amount'=>49, 'description'=>'Any kind of description'])
            ->assertStatus(self::HTTP_FORBIDDEN)
            ->assertJsonStructure(['errors'=>['amount']]);
    }
}

I'm using laravel framework, so it has provided me some helper methods like postJson to sends request with application/json in the headerBut you can still use pure phpunit framework to sends that kind of requests.

As you can see in the above test, we just wrote our first test case scenario. But when I start running this test, it's going to throw an error of HttpNotfound. because we haven't created the api. This is the beauty of TDD. to write the test before the code.

We can now start creating our API in routes/api.php

Route::post('user/charge', 'HomeController@charge');

And regarding that API we need to create the controller

class UserController extends Controller
{
    public function charge()
    {

    }
}

 

Now if we run the test again, it'll throw another error Expected status code 422 but received 200. its because we didn't have any validation to throw an error. 

public function charge(ValidateCharge $request)
{

}

 We have added a new request validation class. 

class ValidateCharge extends FormRequest
{
   
    public function rules()
    {
        return [
            'amount'=>'required|min:50|integer',
            'description'=>'required|string'
        ];
    }
}

As you can see in the above code, we have added required|min:50|integer . The only reason that we have used 50 it's because stripe is not accepting less than 50 cents.

Now if we run the test again, we are going to see the beautiful green success 

After finishing with the functional tests, we are now going to write unit tests, so basically we need to charge a user for a specific amount. If you have ever used stripe then you know what method we should use otherwise for those who don't know which method is used for charging a user I'll explain about it.

We can use the following piece of code to charge the user for a specific amount 

// When it's time to charge the customer again, retrieve the customer ID.
$charge = \Stripe\Charge::create([
    'amount' => 1500, // $15.00 this time
    'currency' => 'usd',
    'customer' => $customer_id, // Previously stored, then retrieved
]);

The first unit test scenario is to test the stripe charge class.

 /**
* @test
*/
public function charge_should_call_stripe_charge_with_specific_parameters()
{

    $user = factory(User::class)->make();
    Auth::shouldReceive('User')->andReturn($user);

    $validateCharge = new ValidateCharge();
    $validateCharge->merge(['amount'=>2500, 'description'=>'Some descriptions']);

    $mockCharge = \Mockery::mock('alias:'.Charge::class);
    $mockCharge->shouldReceive('create')->once()->with([
        'amount' => $validateCharge->amount,
        'currency' => 'eur',
        'customer' => $user->customer_id,
        'description'=>$validateCharge->description
    ]);

    $this->app->make(HomeController::class)->charge($validateCharge);
}

Explaining the above code.

  1. We have made a new user (In unit testing you need to not touch the database as much as possible and keep your code isolated from what is happening in the database)
  2. We have mocked the Auth class and returned the mocked user
  3. Created a new request with specific parameters
  4. Now we have used the alias to mock public static methods.
  5. We have resolved the controller from the container and called the charge method with the request that we have created.

The test going to fail because we have not written any logic in the charge method. so let's start with the real code now.

  1. We should get the logged-in user
  2. We should call create from charge class with specific parameters
public function charge(ValidateCharge $request)
{

    $user = Auth::user();

    $charge = Charge::create([
        'amount' => $request->amount,
        'currency' => 'eur',
        'customer' => $user->customer_id,
        'description'=>$request->description
    ]);
}

Mmm, seems very easy? Okay, what next?

We need to write another scenario to test if the transaction is succeeded or not 

If you checked the charge class from stripe you can see there is a status properties which you can access to check the status of the transaction.

We can start with the second scenario to test if the status of the transaction is succeeded or not. 

/**
* @test
*/
public function charge_should_call_stripe_charge_and_expect_success_status()
{

    $user = factory(User::class)->make();
    Auth::shouldReceive('User')->andReturn($user);

    $validateCharge = new ValidateCharge();
    $validateCharge->merge(['amount'=>2500, 'description'=>'Some descriptions']);

    $object = new \stdClass();
    $object->status = 'succeeded';

    $mockCharge = \Mockery::mock('alias:'.Charge::class);
    $mockCharge->shouldReceive('create')->withAnyArgs()->andReturn($object);

    $response = $this->app->make(HomeController::class)->charge($validateCharge);

    $this->assertEquals(self::HTTP_OKAY, $response->getStatusCode());
}

After running the above test we will get an error because we are not returning any response object. so we can change our code in charge method to something like this

public function charge(ValidateCharge $request)
{

    $user = Auth::user();

    $charge = Charge::create([
        'amount' => $request->amount,
        'currency' => 'eur',
        'customer' => $user->customer_id,
        'description'=>$request->description
    ]);

    if($charge->status === 'succeeded'){
        return response()
               ->json(['message'=>'You have been charged for '.$request->amount.' eur successfully']);
    }
}

Now our test should work like a charm, But wait. that's it no more scenarios, mmm what should we do if the status was not succeeded? 

Guess what, we have another scenario. if the status is not succeeded then we should return a response with failed status, it could be 400 bad request. I'm not going to write the test because you already know how to write a test for failure status.

But I would like to know what will happen if the charge::create method didn't process the request for any reason or something bad happened in the middle of the request, how should we really handle it? we can have a try-catch for that. so we need to have another scenario to make the charge::create throw an error. and of-course in the test we should get the corresponding response.

We can now start with the new scenario and write tests for it.

/**
* @test
* @expectedException HttpException
/
public function charge_should_catch_the_error_if_something_bad_happened_while_calling_stripe_charge()
{
    $user = factory(User::class)->make();
    Auth::shouldReceive('User')->andReturn($user);

    $validateCharge = new ValidateCharge();
    $validateCharge->merge(['amount'=>2500, 'description'=>'Some descriptions']);

    $mockCharge = Mockery::mock('alias:'.Charge::class);
    $mockCharge->shouldReceive('create')->withAnyArgs()->andThrowExceptions([new Exception()]);

    $this->app->make(HomeController::class)->charge($validateCharge);
}

In the above code, you can see that we have mocked the class with alias and then we threw an exception, but if we run the above test we will see red light saying that the code throws an exception. now we should handle the code in a way so it'll return appropriate response while the charge method throws an error. so the code will be something like this

public function charge(ValidateCharge $request)
{

    $user = Auth::user();

    try{
        $charge = Charge::create([
            'amount' => $request->amount,
            'currency' => 'eur',
            'customer' => $user->customer_id,
            'description'=>$request->description
       ]);


        if($charge->status === 'succeeded'){
            return response()->json(['message'=>'You have been charged for '.$request->amount.' eur successfully']);
        } else {
            return response()->json(['message'=>'There is a problem in charging, please contact service support', 400]);
        }

    } catch (\Throwable $exception){
        return response()->json(['message'=>'There is a problem in charging, please contact service support', 500]);
    }

}

After changing the above code, now the test should be run smoothly and you should now be able to see the green success light again. Well done. we have created a handful of scenarios and we just used TDD to write the code. we only have forgotten one more test and it's related to the user balance, we should update the user balance after charging him. so we can write a test to check if the user balance has been updated or not. this test can go under the integration category because we want to test the database.

Let's write the test regarding updating user balance.

/**
* @test
*/
public function charge_should_update_the_balance_if_all_goes_well()
{
    $user = factory(User::class)->create();
    $this->actingAs($user);

    $validateCharge = new ValidateCharge();
    $validateCharge->merge(['amount'=>2500, 'description'=>'Some descriptions']);

    $object = new stdClass();
    $object->status = 'succeeded';

    $mockCharge = Mockery::mock('alias:'.Charge::class);
    $mockCharge->shouldReceive('create')->withAnyArgs()->andReturn($object);

    $this->app->make(HomeController::class)->charge($validateCharge);

    $this->assertDatabaseHas('users', ['id'=>$user->id, 'balance'=>2500]);
}

The above piece of code will throw an error because we need to change the code in charge method, so we go straight to the code and update the user balance.

$charge = Charge::create([
    'amount' => $request->amount,
    'currency' => 'eur',
    'customer' => $user->customer_id,
    'description'=>$request->description
]);

$user->update(['balance'=>$request->amount]);

Okay, after we changed the code we can now run the tests again and we gonna have a green light. I think we have written all of the scenarios. now you can understand how much is easy to write tests before any code. it'll make you think before doing any action. yes, it'll make you slow in some part of developing but also it gives you the insurance that your codes will work as you expected. Hope you enjoyed the article :).