Given I want to install a go library
When I run go get github.com/author/repo
Then I should not see the error unable to connect to go.googlesource.com

If the above scenario isn’t working out for you, make sure you don’t have this in your .gitconfig:

1
2
[url "git://"]
  insteadOf = https://

This is a follow up to my previous post. I recently switched to vim-plug which I think is nicer to use. You can keep all dependencies together in one line and specify post-install commands, which is not possible with Vundle.

And so, my vundle-cli is now obsolete for my own use. I have to write another CLI and this time it will take care of multiple Vim plugin managers: vimplugin-cli

VimPlugin CLI gif

Often time when your project starts to grow, the test suite also starts to get bigger, i.e slower CI.

You will notice that there are tests that hardly ever fail. Maybe we shouldn’t run these every commit? How about run these and the full test suite on schedule such as once a day?

For a Laravel project that is hosted on Github, here is the idea:

1. Create a branch ci-full-tests off your main branch and set up your CI to run tests for this branch
2. Get a Github OAuth token
3. Create a console command to merge the main branch to ci-full-tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

namespace App\Console\Commands;

use GuzzleHttp\Client;
use Illuminate\Console\Command;

class TriggerCIToRunFullTests extends Command
{
    /**
    * The name and signature of the console command.
    *
    * @var string
    */
    protected $signature = 'ci:full-tests';

    /**
    * The console command description.
    *
    * @var string
    */
    protected $description = 'Trigger CI to run the full tests';

    /**
    * @var Client
    */
    protected $client;

    /**
    * Create a new command instance.
    *
    * @return void
    */
    public function __construct(Client $client)
    {
        parent::__construct();

        $this->client = $client;
    }

    /**
    * Execute the console command.
    *
    * @return mixed
    */
    public function handle()
    {
        if (app()->environment() !== 'staging') {
            return;
        }

        $this->client->post("https://api.github.com/repos/YOUR_ORG/REPO_NAME/merges", [
            'headers' => [
                'X-GitHub-Media-Type' => 'application/vnd.github.v3+json',
                'Authorization' => 'token ' . env('GITHUB_OAUTH_TOKEN'),
            ],
            'json' => [
                'base' => 'ci-full-tests',
                'head' => env('BRANCH_FOR_CODESHIP_FULL_TESTS', 'master'),
                'commit_message' => '[Schedule] Run full tests',
            ],
        ]);
    }
}

4. Schedule to run this command once a day

1
2
3
// app/Console/Kernel.php

$schedule->command('ci:full-tests')->daily();

5. Add an if condition in your CI bash script

1
2
3
4
5
6
7
8
ci_branch=`printenv GIT_BRANCH`

if [[ $ci_branch == "ci-full-tests" ]]
then
    vendor/bin/behat --tags=~@wip
else
    vendor/bin/behat --tags='@important'
fi

Response codes such ass 200, 404, 500 are very common response codes. Each code has their own meanings and so of course make sure you use them correctly.

I just discovered a case where our API wasn’t returning a JSON and the Mule app started to throw exception when it called the API.

What happens is:

1
return Response::json(['data' => [], 'message' => 'Uh oh'], 204);

is considered as an empty response due to Symfony\Component\HttpFoundation\Response treating code 204 as empty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public function prepare(Request $request)
{
    $headers = $this->headers;

    if ($this->isInformational() || $this->isEmpty()) {
        $this->setContent(null);
        $headers->remove('Content-Type');
        $headers->remove('Content-Length');
        //....

    }
    //....
}

//...

/**
 * Is the response empty?
 *
 * @return bool
 */
public function isEmpty()
{
    return in_array($this->statusCode, array(204, 304));
}

I just wrote a PHP library baopham/tree-parser to parse a specific tree content to PHP objects.

This library can parse this tree content:

1
2
3
4
5
6
7
8
9
10
   Root
    |- Level 1 - Order 1
      |- Level 2 - Order 2
        |- Level 3 - Order 3
        |- Level 3 - Order 4
      |- Level 2 - Order 5
    |- Level 1 - Order 6
      |- Level 2 - Order 7
        |- Level 3 - Order 8
          |- Level 4 - Order 9

into this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
foreach ($root->children as $child) {
    print $child->name;

    print $child->order;

    print $child->level;

    print_r($child->children);

    print_r($child->children[0]->children);

    print $child->children[0]->parent === $child;
}

print $root->isRoot

// assuming we got the $lastLeaf after done traversing down.
print $lastLeaf->parent->parent->parent->parent === $root

Advanced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$tree = <<<TREE
  Root
    |- Level 1 - Order 1
      |- Level 2 - Order 2
        |- Level 3 - Order 3
        |- Level 3 - Order 4
      |- Level 2 - Order 5
    |- Level 1 - Order 6
      |- Level 2 - Order 7
        |- Level 3 - Order 8
          |- Level 4 - Order 9
TREE;

$parser = new BaoPham\TreeParser($tree);

$parser->parse();

$structure = $parser->getStructure();

// Get nodes at level 3
$level3Nodes = $structure[3];
// Get node at level 3, order 4
$node = $structure[3][4];

// Get last leaf
$orderedNodes = $parser->getOrderedNodes();
$lastLeaf = $orderedNodes[count($orderedNodes) - 1];

Follow up from my previous post, here is an example feature (similar to the feature from the original post)

behat.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
default:
    suites:
      product_payment_domain:
        contexts: [ ProductPaymentDomainContext ]
        filters:  { tags: '@product_payment' }
      product_payment_ui:
        contexts: [ ProductPaymentUIContext ]
        filters:  { tags: '@product_payment&&@critical' }
    gherkin:
      filters:
        tags: ~@wip
    extensions:
        Laracasts\Behat:
            #env_path: .env.behat
        Behat\MinkExtension:
            base_url: https://e-commerce.dev
            default_session: laravel
            laravel: ~
            selenium2:
              wd_host: "http://selenium-server.dev:4444/wd/hub"
            browser_name: chrome

Feature and Scenarios

How To Write Scenarios

Focus on describing the problem you’re trying to solve, rather than focusing on the “how” and the “look”. For example, don’t mention anything about “I click button x” or “I go page /path/to/page” or “I should see ABC on sidebar” or “I wait” in your scenarios.

Read more here: Introducing Modelling by Example

1
2
3
4
5
6
7
8
9
10
11
12
13
Feature: Product payment
  In order to receive products
  As a customer
  I need to be able to pay for the products that I have selected

  Scenario: Buying a product that is out of stock
    Given I have put the product "Windows ME" in my basket a few days before
    And product "Window ME" is now out of stock
    Then I should not be able to checkout because "Window ME" is out of stock

  Scenario: Cannot buy if product is restricted in the account's country
    Given I am located in a country that does not allow product "FooBar"
    Then I cannot purchase this product

How I would implement the steps in the Domain Context class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
 * @Given I have put the product :product in my basket a few days before
 */
public function iHavePutTheProductInMyBasket($product)
{
    $this->product = factory(Product::class)->create(['name' => $product]);

    $this->basket->put($this->product);
}

/**
 * @Given product :product is now out of stock
 */
public function ProductIsOutOfStock($product)
{
    $this->product->count = 0;

    $this->product->save();
}

/**
 * @Then I should not be able to checkout because :product is out of stock
 */
public function iShouldNotBeAbleToCheckoutBecauseProductIsOutOfStock($product)
{
    $this->assertException(function () {
        $this->basket->checkout();
    }, ProductIsOutOfStock::class);
}

/**
 * @Given I am located in a country that does not allow product :product
 */
public function iAmLocatedInACountryThatDoesNotAllowProduct($product)
{
    $this->currentUser = factory(User::class)->create();

    $this->product = factory(Product::class)->create(['name' => $product]);

    $this->currentUser->country->restricted_products()->sync([$this->product->id]);
}

/**
 * @Then I cannot purchase this product
 */
public function iCannotPurchaseThisProduct()
{
    $this->currentUser->cannot('purchase', $this->product);
}

where the method assertException is a custom method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * @param Closure $closure
 * @param string $exception
 */
protected function assertException(\Closure $closure, $exception)
{
    $fail = false;

    try {
        $closure();

        $fail = true;
    } catch (\Exception $e) {
        if (!($e instanceof $exception)) {
            $fail = true;
        }
    }

    if ($fail) {
        $this->fail("Expected $exception to be thrown");
    }
}

and if you follow my previous post, the assertEquals is forwarded to the PHPUnit_Framework_Assert class.

Here is an interesting read talking about how to “do a Domain-Driven Design while doing Behaviour-Driven Development’s red-green-refactor cycle”: Introducing Modelling by Example

If you like the author’s idea and want to apply this to your Laravel projects, here is something you can do to get most (if not all) Laravel PHPUnit helper methods in your Behat Domain Context class:

  • Install Behat-Laravel-Extension
  • Have a base Context class. I call it DomainContext but you could do something similar for the default FeatureContext. Having a separate abstract parent class lets me divide my context classes into 2 categories: UI and Domain.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

use App\User;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\MinkExtension\Context\MinkContext;
use Illuminate\Foundation\Testing\ApplicationTrait;
use Illuminate\Foundation\Testing\CrawlerTrait;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Laracasts\Behat\Context\KernelAwareContext;
use PHPUnit_Framework_Assert as PHPUnit;
use Symfony\Component\HttpKernel\HttpKernelInterface;


abstract class DomainContext extends MinkContext implements KernelAwareContext, SnippetAcceptingContext
{
    use ApplicationTrait;
    use CrawlerTrait;

    protected $baseUrl = 'http://localhost';

    /**
     * @var User
     */
    protected $currentUser;

    public function setApp(HttpKernelInterface $app)
    {
        $this->app = $app;
    }

    public function be(User $user)
    {
        Session::start();

        Auth::loginUsingId($user->id);

        $this->currentUser = $user;
    }

    public function __call($name, array $arguments)
    {
        forward_static_call_array([PHPUnit::class, $name], $arguments);
    }
}
  • Extend this base class for all your Domain Context classes then you will have access to methods such as:
1
2
3
4
5
6
7
8
9
$this->be($user);

$this->get('users');

$this->post('users', ['_token' =>  csrf_token(), 'email' => 'foo@bar.com']);

$this->expectsJobs(SendInvitationEmail::class);

$this->seeInDatabase('users', ['email' => 'foo@bar.com']);

If you want to run selenium server on the host machine and behat in VirtualBox, this setup could work for you:

behat.yml
1
2
3
4
5
6
7
default:
    extensions:
        Behat\MinkExtension:
            base_url: https://foobar.dev
            selenium2:
                wd_host: "http://selenium-server.dev:4444/wd/hub"
            browser_name: chrome

In your virtualbox, edit the /etc/hosts and add a line:

/etc/hosts
1
10.0.2.2 selenium-server.dev

Then run your selenium server on your host machine.

Here is a way to create reusable strings / numbers / etc. in CloudFormation.

1 – Create Lambda resource to build the values you need.

2 – Create a custom resource to store the value returned from the Lambda

Example: Here we have a custom resource AppInfo to hold the App information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"Resources": {
  "AppInfo": {
    "Type": "Custom::AppInfo",
    "Properties": {
      "ServiceToken": { "Fn::GetAtt": ["GetAppInfo", "Arn"] },
      "BuildVersion": { "Ref": "BuildVersion" }
    }
  },
  "GetAppInfo": {
    "Type": "AWS::Lambda::Function",
    "Properties": {
      "Handler": "index.handler",
      "Role": { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] },
      "Code": {
        "ZipFile":  { "Fn::Join": ["", [
          "var response = require('cfn-response');",
          "exports.handler = function(event, context) {",
          "   var username = '",
          {
              "Fn::Join": [ "_", [ {"Ref": "AppName"}, {"Ref": "AppEnv"} ] ]
          },
          "';",
          "   var email = '",
          {
              "Fn::Join": [ "", [ {"Ref": "AppName"}, "+", {"Ref": "AppEnv"}, "@gmail.com" ] ]
          },
          "';",
          "   var responseData = { Username: username, Email: email };",
          "   response.send(event, context, response.SUCCESS, responseData);",
          "};"
          ]]
        }
      },
      "Runtime": "nodejs"
    }
  },
  "LambdaExecutionRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
      "AssumeRolePolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }]
      },
      "Path": "/",
      "Policies": [{
        "PolicyName": "root",
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{ "Effect": "Allow", "Action": ["logs:*"], "Resource": "arn:aws:logs:*:*:*" }]
        }
      }]
    }
  }
}

3 – Reference the custom resource with Fn::GetAtt:

1
2
3
{ "Fn::GetAtt": [ "AppInfo", "Username" ] },
{ "Fn::GetAtt": [ "AppInfo", "Email" ] }
...

4 – Create a parameter BuildVersion. This is the key to make sure the custom resource AppInfo is updated when you update your parameters AppName or AppEnv. Without it, when you update these 2 parameters, CloudFormation will only update Lambda function and leave the resource AppInfo the same without sending another request to GetAppInfo for the latest value.

1
2
3
4
5
6
7
"Parameters": {
  "BuildVersion": {
    "Description": "Build Version. Needs to be different everytime you make an update",
    "Type": "String",
    "MinLength": 1
  }
}

With this package baopham/dynamodb, you can do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Campaign extends BaoPham\DynamoDb\DynamoDbModel
{
    protected $table = 'dynamodb_table_for_campaign';

    protected $fillable = ['name', 'description', 'status'];
}

$campaign = new Campaign();

$campaign->find('campaign-id'); // it's best to use UUID instead of incremented id - less work since you have to set the id attribute yourself.

// or
$campaign->where('name', 'foo')
        ->where('status', true)
        ->get();

// or
$campaign->first();

// or
$campaign->update(['name' => 'foo2', 'description' => 'bar2', 'status' => false]);

// or
$campaign->find('campaign-id')->delete();

For more, README, tests and code are available here: https://github.com/baopham/laravel-dynamodb