I have done a follow-up Mentoring style video to support my last one. This video shows how to fix some of the issues in Git I came across in my last mentoring video!
I took some advice from a colleague who noticed that I skipped past a couple of issues with my Git setup, so I re-did them :) I hope this makes sense, and at 35 minutes, is a bit more understandable than the last 1h15 video!
If you’ve ever wondered how I use Ansible and Inspec, or wondered why some of my Vagrant files look like they do, well, I want to start recording some “mentor” style videos… You know how, if you were sitting next to someone who’s a mentor to you, and you watch how they build a solution.
This was my second attempt at recording a mentoring style video yesterday, the first was shown to the Admin Admin Podcast listeners group on Telegram, and then sacrificed to the demo gods (there were lots of issues in that first video) never to be seen again.
From a tooling perspective, I’m using a remote virtual machine running Ubuntu Mate 18.04 over RDP (to improve performance) with xrdp and Remmina, OBS is running locally to record the content, and I’m using Visual Studio Code, git, Vagrant and Virtualbox, as well as Ansible and Inspec.
I’ve got a small Ansible script that I bundle up on Ubuntu boxes to do apt updates. This was originally a one-statement job, but I’ve added a few lines to it, so I thought I’d explain what I’m doing (more for myself, for later!)
Initally, I just had a task to do apt: upgrade=full update_cache=yes autoremove=yes autoclean-yes but if you’re running the script over and over again, well, this gets slow… So I added a tweak!
Here it is folks, in all it’s glory!
- hosts: all
tasks:
- name: Get stat of last run apt
stat:
path: /var/cache/apt/pkgcache.bin
register: apt_run
- name: "Apt update, Full-upgrade, autoremove, autoclean check"
debug:
msg: "Skipping apt-update, etc. actions as apt update was run today"
when: "'%Y-%m-%d' | strftime(apt_run.stat.mtime) in ansible_date_time.date"
- name: "Apt update, Full-upgrade, autoremove, autoclean"
apt:
upgrade: full
update_cache: yes
autoremove: yes
autoclean: yes
when: "'%Y-%m-%d' | strftime(apt_run.stat.mtime) not in ansible_date_time.date"
What does this do? Well, according to this AskUbuntu post, the best file to check if an update has been performed is /var/cache/apt/pkgcache.bin, so we check the status of that file. Most file systems available to Linux distributions provide the mtime – or “last modified time”. This is returned in the number of seconds since UTC 00:00:00 on the Unix Epoch (1970-01-01), so we need to convert that to a date., which we return as YYYY-MM-DD (e.g. today is 2020-01-06) and then compare that to what the system thinks today is. If the dates don’t equate (so one string doesn’t match the other – in other words, apt update wasn’t run today), it runs the update. If the dates do match up, we get a statement saying that apt update was already run.
Having got a VM stood up in Azure, I wanted to build a VM in AWS, after all, it’s more-or-less the same steps. Note, this is a work-in-progress, and shouldn’t be considered “Final” – this is just something to use as *your* starting block.
What do you need?
You need an AWS account for this. If you’ve not got one, signing up for one is easy, but bear in mind that while there are free resource on AWS (only for the first year!), it’s also quite easy to suddenly enable a load of features that cost you money.
Best practice suggests (or rather, INSISTS) you shouldn’t use your “root” account for AWS. It’s literally just there to let you define the rest of your admin accounts. Turn on MFA (Multi-Factor Authentication) on that account, give it an exceedingly complex password, write that on a sheet of paper, and lock it in a box. You should NEVER use it!
Create your admin account, log in to that account. Turn on MFA on *that* account too. Then, create an “Access Token” for your account. This is in IAM (Identity and Access Management). These are what we’ll use to let Terraform perform actions in AWS, without you needing to actually “log in”.
On my machine, I’ve put the credentials for this in /home/<MYUSER>/.aws/credentials and it looks like this:
This file should be chmod 600 and make sure it’s only your account that can access this file. With this token, Terraform can perform *ANY ACTION* as you, including anything that charges you money, or creating servers that can mine a “cryptocurrency” for someone malicious.
I’m using Windows Subsystem for Linux (WSL). I’m using the Ubuntu 18.04 distribution obtained from the Store. This post won’t explain how to get *that*. Also, you might want to run Terraform on Mac, in Windows or on Linux natively… so, yehr.
Next, we need to actually install Terraform. Excuse the long, unwrapped code block, but it gets what you need quickly (assuming the terraform webpage doesn’t change any time soon!)
Before you can build your first virtual machine on AWS, you need to stand up the supporting infrastructure. These are:
An SSH Keypair (no password logins here!)
A VPC (“Virtual Private Cloud”, roughly the same as a VNet on Azure, or somewhat like a L3 switch in the Physical Realm).
An Internet Gateway (if your VPC isn’t classed as “the default one”)
A Subnet.
A Security Group.
Once we’ve got these, we can build our Virtual Machine on EC2 (“Elastic Cloud Compute”), and associate a “Public IP” to it.
To quote my previous post:
One quirk with Terraform, versus other tools like Ansible, is that when you run one of the terraform commands (like terraform init, terraform plan or terraform apply), it reads the entire content of any file suffixed “tf” in that directory, so if you don’t want a file to be loaded, you need to either move it out of the directory, comment it out, or rename it so it doesn’t end .tf. By convention, you normally have three “standard” files in a terraform directory – main.tf, variables.tf and output.tf, but logically speaking, you could have everything in a single file, or each instruction in it’s own file.
For the sake of editing and annotating the files for this post, these code blocks are all separated, but on my machine, they’re all currently one big file called “main.tf“.
In that file, I start by telling it that I’m working with the Terraform AWS provider, and that it should target my nearest region.
If you want to risk financial ruin, you can put things like your access tokens in here, but I really wouldn’t chance this!
Next, we create our network infrastructure – VPC, Internet Gateway and Subnet. We also change the routing table.
I suspect, if I’d created the VPC as “The Default” VPC, then I wouldn’t have needed to amend the routing table, nor added an Internet Gateway. To help us make the routing table change, there’s a “data” block in this section of code. A data block is an instruction to Terraform to go and ask a resource for *something*, in this case, we need AWS to tell Terraform what the routing table is that it created for the VPC. Once we have that we can ask for the routing table change.
AWS doesn’t actually give “proper” names to any of it’s assets. To provide something with a “real” name, you need to tag that thing with the “Name” tag. These can be practically anything, but I’ve given semi-sensible names to everything. You might want to name everything “main” (like I nearly did)!
We’re getting close to being able to create the VM now. First of all, we’ll create the Security Groups. I want to separate out my “Allow Egress Traffic” rule from my “Inbound SSH” rule. This means that I can clearly see what hosts allow inbound SSH access. Like with my Azure post, I’m using a “data provider” to get my public IP address, but in a normal “live” network, you’d specify a collection of valid source address ranges.
Last steps before we create the Virtual Machine. We need to upload our SSH key, and we need to find the “AMI” (AWS Machine ID) of the image we’ll be using. To create the key, in this directory, along side the .tf files, I’ve put my SSH public key (called id_rsa.pub), and we load that key when we create the “my_key” resource. To find the AMI, we need to make another data call, this time asking the AMI index to find the VM with the name containing ubuntu-bionic-18.04 and some other stuff. AMIs are region specific, so the image I’m using in eu-west-2 will not be the same AMI in eu-west-1 or us-east-1 and so on. This filtering means that, as long as the image exists in that region, we can use “the right one”. So let’s take a look at this file.
So, now we have everything we need to create our VM. Let’s do that!
In here, we specify a “user_data” file to upload, in this case, the contents of a file – CloudDev.sh, but you can load anything you want in here. My CloudDev.sh is shown below, so you can see what I’m doing with this file :)
So, having created all this lot, you need to execute the terraform workload. Initially you do terraform init. This downloads all the provisioners and puts them into the same tree as these .tf files are stored in. It also resets the state of the terraform discovered or created datastore.
Next, you do terraform plan -out tfout. Technically, the tfout part can be any filename, but having something like tfout marks it as clearly part of Terraform. This creates the tfout
file with the current state, and whatever needs to change in the
Terraform state file on it’s next run. Typically, if you don’t use a
tfout file within about 20 minutes, it’s probably worth removing it.
Finally, once you’ve run your plan stage, now you need to apply it. In this case you execute terraform apply tfout. This tfout is the same filename you specified in terraform plan. If you don’t include -out tfout on your plan (or even run a plan!) and tfout in your apply, then you can skip the terraform plan stage entirely.
Once you’re done with your environment, use terraform destroy to shut it all down… and enjoy :)
I’m strongly in the “Ansible is my tool, what needs fixing” camp, when it comes to Infrastructure as Code (IaC) but, I know there are other tools out there which are equally as good. I’ve been strongly advised to take a look at Terraform from HashiCorp. I’m most familiar at the moment with Azure, so this is going to be based around resources available on Azure.
Late edit: I want to credit my colleague, Pete, for his help getting started with this. While many of the code samples have been changed from what he provided me with, if it hadn’t been for these code samples in the first place, I’d never have got started!
Late edit 2: This post was initially based on Terraform 0.11, and I was prompted by another colleague, Jon, that the available documentation still follows the 0.11 layout. 0.12 was released in May, and changes how variables are reused in the code. This post now *should* follow the 0.12 conventions, but if you spot something where it doesn’t, check out this post from the Terraform team.
As with most things, there’s a learning curve, and I struggled to find a “simple” getting started guide for Terraform. I’m sure this is a failing on my part, but I thought it wouldn’t hurt to put something out there for others to pick up and see if it helps someone else (and, if that “someone else” is you, please let me know in the comments!)
Pre-requisites
You need an Azure account for this. This part is very far outside my spectrum of influence, but I’m assuming you’ve got one. If not, look at something like Digital Ocean, AWS or VMWare :) For my “controller”, I’m using Windows Subsystem for Linux (WSL), and wrote the following notes about getting my pre-requisites.
Building the file structure
One quirk with Terraform, versus other tools like Ansible, is that when you run one of the terraform commands (like terraform init, terraform plan or terraform apply), it reads the entire content of any file suffixed “tf” in that directory, so if you don’t want a file to be loaded, you need to either move it out of the directory, comment it out, or rename it so it doesn’t end .tf. By convention, you normally have three “standard” files in a terraform directory – main.tf, variables.tf and output.tf, but logically speaking, you could have everything in a single file, or each instruction in it’s own file. Because this is a relatively simple script, I’ll use this standard layout.
The actions I’ll be performing are the “standard” steps you’d perform in Azure to build a single Infrastructure as a Service (IAAS) server service:
Create your Resource Group (RG)
Create a Virtual Network (VNET)
Create a Subnet
Create a Security Group (SG) and rules
Create a Public IP address (PubIP) with a DNS name associated to that IP.
Create a Network Interface (NIC)
Create a Virtual Machine (VM), supplying a username and password, the size of disks and VM instance, and any post-provisioning instructions (yep, I’m using Ansible for that :) ).
I’m using Visual Studio Code, but almost any IDE will have integrations for Terraform. The main thing I’m using it for is auto-completion of resource, data and output types, also the fact that control+clicking resource types opens your browser to the documentation page on terraform.io.
So, creating my main.tf, I start by telling it that I’m working with the Terraform AzureRM Provider (the bit of code that can talk Azure API).
This simple statement is enough to get Terraform to load the AzureRM, but it still doesn’t tell Terraform how to get access to the Azure account. Use az login from a WSL shell session to authenticate.
Next, we create our basic resource, vnet and subnet resources.
But wait, I hear you cry, what are those var.something bits in there? I mentioned before that in the “standard” set of files is a “variables.tf” file. In here, you specify values for later consumption. I have recorded variables for the resource group name and location, as well as the VNet name and subnet name. Let’s add those into variables.tf.
When you’ve specified a resource, you can capture any of the results from that resource to use later – either in the main.tf or in the output.tf files. By creating the resource group (called “rg” here, but you can call it anything from “demo” to “myfirstresourcegroup”), we can consume the name or location with azurerm_resource_group.rg.name and azurerm_resource_group.rg.location, and so on. In the above code, we use the VNet name in the subnet, and so on.
After the subnet is created, we can start adding the VM specific parts – a security group (with rules), a public IP (with DNS name) and a network interface. I’ll create the VM itself later. So, let’s do this.
BUT WAIT, what’s that ${trimspace(data.http.icanhazip.body)}/32 bit there?? Any resources we want to load from the terraform state, but that we’ve not directly defined ourselves needs to come from somewhere. These items are classed as “data” – that is, we want to know what their values are, but we aren’t *changing* the service to get it. You can also use this to import other resource items, perhaps a virtual network that is created by another team, or perhaps your account doesn’t have the rights to create a resource group. I’ll include a commented out data block in the overall main.tf file for review that specifies a VNet if you want to see how that works.
In this case, I want to put the public IP address I’m coming from into the NSG Rule, so I can get access to the VM, without opening it up to *everyone*. I’m not that sure that my IP address won’t change between one run and the next, so I’m using the icanhazip.com service to determine my IP address. But I’ve not defined how to get that resource yet. Let’s add it to the main.tf for now.
So, we’re now ready to create our virtual machine. It’s quite a long block, but I’ll pull certain elements apart once I’ve pasted this block in.
So, this is broken into four main pieces.
Virtual Machine Details. This part is relatively sensible. Name RG, location, NIC, Size and what happens to the disks when the machine powers on. OK.
OS basics: VM Hostname, username of the first user, and it’s password. Note, if you want to use an SSH key, this must be stored for Terraform to use without passphrase. If you mention an SSH key here, as well as a password, this can cause all sorts of connection issues, so pick one or the other.
And lastly, provisioning. I want to use Ansible for my provisioning. In this example, I have a basic playbook stored locally on my Terraform host, which I transfer to the VM, install Ansible via pip, and then execute ansible-playbook against the file I uploaded. This could just as easily be a git repo to clone or a shell script to copy in, but this is a “simple” example.
This part of code is done in three parts – create upload path, copy the files in, and then execute it. If you don’t create the upload path, it’ll upload just the first file it comes to into the path specified.
Each remote-exec and file provisioner statement must include the hostname, username and either the password, or SSH private key. In this example, I provide just the password.
So, having created all this lot, you need to execute the terraform workload. Initially you do terraform init. This downloads all the provisioners and puts them into the same tree as these .tf files are stored in. It also resets the state of the terraform discovered or created datastore.
Next, you do terraform plan -out tfout. Technically, the tfout part can be any filename, but having something like tfout marks it as clearly part of Terraform. This creates the tfout file with the current state, and whatever needs to change in the Terraform state file on it’s next run. Typically, if you don’t use a tfout file within about 20 minutes, it’s probably worth removing it.
Finally, once you’ve run your plan stage, now you need to apply it. In this case you execute terraform apply tfout. This tfout is the same filename you specified in terraform plan. If you don’t include -out tfout on your plan (or even run a plan!) and tfout in your apply, then you can skip the terraform plan stage entirely.
When I ran this, with a handful of changes to the variable files, I got this result:
Once you’re done with your environment, use terraform destroy to shut it all down… and enjoy :)
The full source is available in the associated Gist. Pull requests and constructive criticism are very welcome!
Featured image is “Seca” by “Olearys” on Flickr and is released under a CC-BY license.
I want to preach for a few minutes about Containers in Firefox. This is not like Docker containers, a Snap Package (using cgroups), or Shipping Containers, but instead a way of describing how each tab protects you from tracking.
Here’s a quick lesson in how the web works. Each website you visit, when you get the HTML page, it might *also* ask you store a small text file, a “Cookie” that then gets handed *back* to that site the next time you visit. It’s an easy way of saying “I’ve been here before, you know me already”.
This doesn’t just happen when you visit a web page (unless the web page is really *really* simple), it also happens for each resource on that page. If the page also asks for an image (say, the logo of a social media network), a script (say, a banner bar from an advertising network) or a font (yep, web fonts are also a thing!), each one of those also gets to say “here’s a Cookie, keep it for the next time you come back”.
For a few years, there have been ad-blockers (my favourite two are “uBlock Origin” and “Privacy Badger“), which can stop the content from ever being loaded… but it’s an arms race. The ad-blockers stop content from being loaded (mostly it’s just to stop the adverts, but the other stuff is a benefit that they’ve kept on doing), then the tracking firms do something else to make it so their content is loaded, and so-on. Firefox also has “Private Browsing Mode”, which can stop “third party cookies” (the ones from each of the additional sources on the page) from being shared… but I always think that Private Browsing mode looks shady.
In the last couple of years, Firefox started an experiment called “Firefox Multi-Account Containers” (or just “Containers” for short). This is designed to create a whole new “state” for each browser tab, that’s shared between those containers.
You can mark particular websites as being part of a particular container, so Twitter, Facebook and GMail all end up in my “Personal” container, whereas the sites I need for work are in the “Work” container.
For a while I was using them to support family members (“I just need you to log into your GMail account for me to have a poke around… let me create a new container for your account”, or “Let’s have a look at why you’re getting those Facebook posts. Can you log in in this container here?”).
Then I needed it to separate out a couple of Twitter accounts I’m responsible for (when I use the “Switch Containers” extension to jump between them)… Then I found a new extension which upgraded how I use them “Temporary Containers“. With a couple of tweaks (see below), this makes every new tab into it’s own container… so it’s a bit like Private Browsing Mode, but one which dynamically turns itself into a “non-private mode” if you hit the right URL.
So, this is my work-flow – it might not work for you, but equally, it might! When I open a new tab, or visit a website that isn’t already categorised as a “Personal”, “Work” (or so-on) container, I get taken to a new “Temporary” container.
I then ask myself if this is something I need to log into with one of my existing containers (e.g. Google, Facebook, Twitter, Github, Azure, AWS etc), and if so, I’ll “Switch Containers” to that container (e.g. Personal).
If I think that I always want to open it here then I’ll click on the “Containers” button in the bar, and select “Always open in ‘Personal'”.
If I’ve categorised something that I need to swap to something else (e.g. Twitter for another account, or a family member’s GMail account), then I explicitly “Switch Containers” or open a tab in that container first, and then go to the website.
If I need a new container for this window, I use the + symbol next to the “Edit Containers” button in the containers button in the window bar.
I also use the “Open Bookmark in Container” extension, for when I’m using bookmarks, as, by default, these can’t be opened in a container. I also use the “Containers Theme” extension, as can be seen by the colour changes in the above screenshots.
While this is fully available for Firefox on Desktop, it’s not yet available on Firefox for Android or Firefox for iOS, and there’s no word on whether it will come at all…
Recently a friend of mine forwarded an email to me about a Wi-fi service he wanted to use from a firm, but he raised some technical questions with them which they seemed to completely misunderstand!
So, let’s talk about the misconceptions of Wi-fi passwords.
People also assume the same thing about Wi-fi. You reached a log in page, so it must be secure, right? It depends. If you didn’t put in a password to access the Wi-fi in the first place (like in the image of the Windows 10 screen, or on my KDE Desktop) then you’re probably using Unsecured Wi-fi.
People like to compare network traffic to “sending things through the post”, notablycomparing E-Mail to “sending a postcard”, versus PGP encrypted E-Mail being compared to “sending a sealed letter”. Unencrypted Wi-fi is like using CB. Anyone who can hear your signal can understand what you are saying… but if you visit a website which uses HTTPS, then it’s like listening to someone saying random numbers over the radio.
So, many of these things can be protected against by using a simple method, that many people who provide Wi-fi don’t do.
Turn on WPA2 (the authentication bit). Even if *everyone* uses the same password (which they’d have to for WPA2), the fact you’re logging into the Access Point means it creates a unique shared secret for your session.
“But hang on”, I hear the guy at the back cry, “you used the same password – how does that work?”
OK, so this is where the fun stuff starts. The password is just part of how you negotiate to get on to the network. There’s a complex beast of a method that explains how get a shared unique secret when you’re passing stuff around “in the clear”, and so as a result, when you first connect to that Wi-fi access point, and you hand over your password, it “Authorises” you on to the network, but then hands you over to the encryption part, where you generate a key and then use that to talk to each other. The encryption is the bit like “HTTPS”, where you make it so that people can’t see what you’re looking at.
“I got told that if everyone used the same password” said a hipster in the front row, “I wouldn’t be able to tell them apart.” Aha, not true. You can have a separate passphrase to access the Wi-fi from the Login page, after all, you’ve got to make sure that people aren’t breaking the rules (which they *TOTALLY* read, before clicking “I agree, just get me on the damn Wi-fi already”) by using your network.
“OK”, says the lady over on the right, “but when I connected to the Wi-fi, they asked me to log in using Facebook – that’s secure, right?”
Um, no. Well, maybe. See, if they gave you a WPA2 password to log into the Wi-fi, and then the first thing you got to was that login screen, then yep, it’s all good! {*} You can browse with (relative) impunity. But if they didn’t… well, not only are they asking you to shout your secrets on the radio, but if you’re really unlucky, the page asking you to log into Facebook might *also* not actually be Facebook, but another website that just looks like Facebook… after all, I’m sure that page you went to complained that it wasn’t Google or Facebook when you tried to open it…
{*} Except for the fact they’re asking you to tell them not only who you are, but who you’re also friends with, where you went to school, what your hobbies are, what groups you’re in, your date of birth and so on.
But anyway. I understand why those login screens are there. They’re asserting that not only do you understand that you mustn’t use their network for bad things, but that if the police come and ask them who used their network to do something naughty, they can say “He said his name was ‘Bob Smith’ and his email address was ‘bob@example.com’, Officer”…
It also means that the “free” service they provide to you, usually at some great expense (*eye roll*) can get them some return on investment (like, they just got your totally-real-and-not-at-all-made-up-email-address… honest, and they also know what websites you visited while you were there, which they can sell on).
So… What to do the next time you “need” Wi-fi, and there’s a free service there? Always use a VPN when you’re not using a network you trust. If the Wi-fi isn’t using WPA2 encryption (even something as simple as “Buy a drink first” is a great passphrase to use!) point them to this page, and tell them it’s virtually pain free (as long as the passphrase is easy to remember, easy to type and doesn’t have too many weird symbols in) and makes their service more safe and secure for their customers…
Over the past few days, as you may have noticed, I’ve been experimenting with PHPUnit, and writing up notes on what I’ve learned. Here’s a biggie, but it’s such a small actual change, I didn’t want to miss it.
So, when you have your autoloader written, you’ll have a function like this (probably):
Load this from your test, or in a bootstrap file (more to come on that particular subject, I think!), like this:
<?php
require_once dirname(__FILE__) . '/../autoloader.php';
class SomeClassTest extends ........
And you’ll probably notice the autoloader doesn’t do anything… but why is this? Because PHPUnit has it’s own autoloader, and you need to chain our autoloader to the end. So, in your autoloader file, add this line to the end:
If you don’t know what hashing is in relation to coding, the long version is here: Cryptographic Hash Function but the short version is that it performs a mathermatical formula to components of the file, string or data, and returns a much shorter number with a slim chance of “collisions”.
I don’t know whether it’s immediately clear to anyone else, but I used to think this was a good idea.
<?php
$password = sha1($_POST['password']);
Then I went to a PHPNW session, and asked someone to take a look at my code, and got a thorough drubbing for not adding a cryptographic salt (wikipedia).
For those who don’t know, a salt is a set of characters you add before or after the password (or both!) to make it so that a simple “rainbow table analysis” doesn’t work (essentially a brute-force attack against the authentication data by hashing lots and lots of strings looking for another hash which matches the stored hash). In order to make it possible to actually authenticate with that string again in the future, the string should be easily repeatable, and a way to do that is to use other data that’s already in the user record.
Unit testing seems like a bit of a dark art when you’re first introduced to it. “Create this new file. Tell it what is supposed to be the result when you run a test, and it’ll tell you if you’re right nor not.”
Let’s start with a pseudocode example:
test->assertTrue(1+1 = 2); // Test returns true, huzzah!
test->assertFalse(1+1 = 3); // Test returns false. Those integers must not have been large enough
I want to use PHPUnit, and for me the easiest way to get this and the rest of the tools I’ll be referring to in this collection of posts is to install “The PHP Quality Assurance Toolchain“. On my Ubuntu install, this was done as follows:
In here, you see we’ve created two files, one contains the class we want to use, and the other contains the tests we will be running.
So, let’s slap on the veneer of coating that these two files need to be valid to test.
/Classes/Config.php
<?php
class Config
{
}
/Tests/Config.php
<?php
include dirname(__FILE__) . '/../Classes/Config.php';
class ConfigTest extends PHPUnit_Framework_TestCase
{
}
So, just to summarise, here we have two, essentially empty classes.
Let’s put some code into the test file.
<?php
include dirname(__FILE__) . '/../Classes/Config.php';
class ConfigTest extends PHPUnit_Framework_TestCase
{
public function testCreateObject()
{
$config = new Config();
$this->assertTrue(is_object($config));
}
}
We can now run this test from the command line as follows:
phpunit Tests/ConfigTest.php
phpunit Tests/01_ConfigTest.php
PHPUnit 3.6.10 by Sebastian Bergmann.
.
Time: 1 second, Memory: 3.00Mb
OK (1 test, 1 assertion)
That was nice and straightforward!
Let’s add some more code!
In ConfigTest, let’s tell it to load some configuration, using a config file.
<?php
include dirname(__FILE__) . '/../Classes/Config.php';
class ConfigTest extends PHPUnit_Framework_TestCase
{
public function testCreateObject()
{
$config = new Config();
$this->assertTrue(is_object($config));
}
public function testLoadConfig()
{
$config = new Config();
$config->load();
}
}
And now when we run it?
PHP Fatal error: Call to undefined method Config::load() in /var/www/PhpBetterPractices/Tests/ConfigTest.php on line 16
Ah, perhaps we need to write some code into /Classes/Config.php
<?php
class Config
{
public function load()
{
include dirname(__FILE__) . '/../Config/default_config.php';
}
}
But, running this, again, we get an error message!
PHPUnit 3.6.10 by Sebastian Bergmann.
.E
Time: 0 seconds, Memory: 3.00Mb
There was 1 error:
1) ConfigTest::testLoadConfig
include(/var/www/PhpBetterPractices/Config/default_config.php): failed to open stream: No such file or directory
/var/www/PhpBetterPractices/Classes/Config.php:7
/var/www/PhpBetterPractices/Classes/Config.php:7
/var/www/PhpBetterPractices/Tests/ConfigTest.php:16
FAILURES!
Tests: 2, Assertions: 1, Errors: 1.
So, we actually need to check that the file exists first, perhaps we should throw an error if it doesn’t? We could also pass the name of the config file to pass to the script, which would let us test more and different configuration options, should we need them.
class Config
{
public function load($file = null)
{
if ($file == null) {
$file = 'default.config.php';
}
$filename = dirname(__FILE__) . '/../Config/' . $file;
if (file_exists($filename)) {
include $filename;
} else {
throw new InvalidArgumentException("File not found");
}
}
}
So, here’s the new UnitTest code:
class ConfigTest extends PHPUnit_Framework_TestCase
{
public function testCreateObject()
{
$config = new Config();
$this->assertTrue(is_object($config));
}
public function testLoadConfig()
{
$config = new Config();
$config->load();
}
/**
* @expectedException InvalidArgumentException
*/
public function testFailLoadingConfig()
{
$config = new Config();
@$config->load('A file which does not exist');
}
}
This assumes the file /Config/default.config.php exists, albeit as an empty file.
So, let’s run those tests and see what happens?
PHPUnit 3.6.10 by Sebastian Bergmann.
...
Time: 0 seconds, Memory: 3.25Mb
OK (3 tests, 2 assertions)
Huzzah! That’s looking good. Notice that to handle a test of something which should throw an exception, you can either wrapper the function in a try/catch loop and, in the try side of the loop, have $this->assertTrue(false) to prevent false positives and in the catch side, do your $this->assertBlah() on the exception. Alternatively, (and much more simplely), use a documentation notation of @expectedException NameOfException and then prefix the function you are testing with the @ symbol. This is how I did it with the test “testFailLoadingConfig()”.
This obviously doesn’t handle setting and getting configuration values, so let’s add those.
Here’s the additions to the Config.php file:
public function set($key = null, $value = null)
{
if ($key == null) {
throw new BadFunctionCallException("Key not set");
}
if ($value == null) {
unset ($this->arrValues[$key]);
return true;
} else {
$this->arrValues[$key] = $value;
return true;
}
}
public function get($key = null)
{
if ($key == null) {
throw new BadFunctionCallException("Key not set");
}
if (isset($this->arrValues[$key])) {
return $this->arrValues[$key];
} else {
return null;
}
}
And the default.config.php file:
<?php
$this->set('demo', true);
And lastly, the changes to the ConfigTest.php file:
public function testLoadConfig()
{
$config = new Config();
$this->assertTrue(is_object($config));
$config->load('default.config.php');
$this->assertTrue($config->get('demo'));
}
/**
* @expectedException BadFunctionCallException
*/
public function testFailSettingValue()
{
$config = new Config();
@$config->set();
}
/**
* @expectedException BadFunctionCallException
*/
public function testFailGettingValue()
{
$config = new Config();
@$config->get();
}
We’ve not actually finished testing this yet. Not sure how I can tell?
phpunit --coverage-text Tests/ConfigTest.php
PHPUnit 3.6.10 by Sebastian Bergmann.
....
Time: 0 seconds, Memory: 3.75Mb
OK (4 tests, 5 assertions)
Generating textual code coverage report, this may take a moment.
Code Coverage Report
2012-05-08 18:54:16
Summary:
Classes: 0.00% (0/1)
Methods: 0.00% (0/3)
Lines: 76.19% (16/21)
@Config::Config
Methods: 100.00% ( 3/ 3) Lines: 76.19% ( 16/ 21)
Notice that there are 5 lines outstanding – probably around the unsetting values and using default values. If you use an IDE (like NetBeans) you can actually get the editor to show you, using coloured lines, exactly which lines you’ve not yet tested! Nice.
So, the last thing to talk about is Containers and Dependency Injection. We’ve already started with the Dependency Injection here – that $config->load(‘filename’); function handles loading config files, or you could just bypass that with $config->set(‘key’, ‘value); but once you get past a file or two, you might just end up with a lot of redundant re-loading of config files, or worse, lots of database connections open.
So, this is where Containers come in (something I horrifically failed to understand before).
Here’s a container:
class ConfigContainer
{
protected static $config = null;
public static function Load()
{
if (self::$config == null) {
self::$config = new Config();
self::$config->load();
}
return self::$Config;
}
}
It’s purpose (in this case) is to load the config class, including any dependencies that you may need for that class, and then return that class to you. You could conceivably create a Database container, or a Request container or a User container with very little extra work, and with a few short calls, have a single function for each of your regular and routine sources of processing data, but without preventing you from being able to easily and repeatably test that data – by not going through the container.
Of course, there’s nothing to stop you just having these created in a registry class, or store them in a global from the get-go, but, I am calling these “Better Practices” after all, and these are considered to be not-so-good-practices.
Just as a note, code from this section can be seen at GitHub, if you want to use them at all.
Update 2012-05-11: Added detail to the try/catch exception catching as per frimkron’s comment. Thanks!