[ad_1]
TL;DR: The private podcast account is set up but not yet recorded; I’m looking to do that sooner rather than later. I’m looking to possibly abandon exclusive Overcast support which I’ll briefly talk about. This article walks through the process of defining the conditions for unit testing and how I’ve decided to go about writing the first set of unit tests.
Building Backcast, Unit Testing
Where Do I Go From Here?
Originally, this post was going to be a combination of setting up PHPUnit and getting up and running with writing unit tests but, if you’ve been reading along, then you saw how well that went over.
Honestly, I’m glad I encountered that early on in the process as it shows the random rabbit holes any of us end up in whenever we’re writing code.
Secondly, after running into a hurdle with authentication with Overcast and deciding to just go with a manual export of podcast feeds, I started looking at a couple of different podcast applications to see how they exported their feeds.
Ultimately, I was looking for a set of standards (which would include common attributes) that I could use to evaluate the validity of a file and to update the backup with only new information. (Of course, the first import is always going to be the largest.)
I asked myself the following questions:
- Do I look for a file with a specific name?
- Do I verify the content of the file?
- Do I accept any XML file?
- Do I need to verify the size or date of the file?
Some of these questions, like many, lead me into thinking about other things, too, but in trying to make as much progress as possible with as little friction as necessary, I decided to do the following:
- Verify the type of file (that is, an
opml
file), - Verify that it’s well-formed or, rather, valid XML,
- Verify four attributes of the XML file that appear to be common across podcast exports. (I’m going to test for the
opml
tag, therss
value for thetype
attribute, theoutline
tag, and thexmlUrl
key.)
If any of these fail, then I’ll assume that I have an incorrect export. That seems to be a reasonable balance for now. And now that I have PHPUnit set up and running correctly, I can start writing unit tests.
Test Data
Before writing the tests, I want to make sure I have a couple of export files that I can test against. This, I hope, will verify the validity of the tests.
I’m creating an exports
directory in the tests
directory and dropping in an export from Overcast and from Pocketcasts.
And now that I’ve that done, I’ve merged the current feature branch into the develop
branch.
A Note About My GitHub Workflow
In case I’ve not described this before, my workflow for this project includes creating a develop
branch off of main and then a corresponding feature
branch off of develop
so that I can work on a single branch.
As it stands, I’m trying to keep each branch with a specific post. I’ve not really shared them yet because it’s a private repository but once I have enough functioning code to throw out into the public, I’ll open the repository.
Maybe by the end of this post, even.
Writing Tests
Before writing tests, I need to do the following:
- set up a core class in the
src
directory, - set up the autoload functionality in the
composer.json
configuration file, - and verify that PHPUnit can access my class
Once that’s done, I can actually get into writing tests. But I need to get this scaffolding done first.
Set Up a Core Class That’s in a Namespace
Initially, I’d started out with a constant defined in the main file, but I’m going to move away from that. The fastest way for me to get from nothing-to-something is to set up a single class, for now, that will encapsulate the functionality I’m trying to test.
This is the part where I rationalize not doing BDUF.
Big Design Up Front (BDUF) is a software development approach in which the program’s design is to be completed and perfected before that program’s implementation is started. It is often associated with the waterfall model of software development.
I share a little bit more in my Scattered Thoughts in the section at the end of this post.
Refactoring the Bootstrap
First, I’m creating a new branch (aptly called feature/add-validation-tests
since that’s what I’ll be doing in this post) and I’m going to clean out the code in the core backcast.php
file, or the bootstrap file, so that I can use it to instantiate an object from the class I’m going to be writing.
This means the bootstrap file will look like this:
#!/usr/local/bin/php
<?php
namespace Backcast;
require 'vendor/autoload.php';
new Backcast();
Note here, though, that I’m including the autoload.php
file generated by Composer.
Define Autoload Functionality
Before going any further, let me share what the composer.json
file looks like:
{
"name": "tommcfarlin/backcast",
"description": "An application used to backup podcast subscriptions via podcast XML exports.",
"require-dev": {
"phpunit/phpunit": "^8"
},
"autoload": {
"psr-4": {
"Backcast\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Backcast\Tests\": "tests"
}
}
}
The two things I want to call out are the autoload
and the autoload-dev
areas. The first section tells composer where to locate files for the autoloader which is ideally used in production environments. The second section tells Composer that only the files in this area are intended for non-production environments.
This means when you run composer dump-autoload --no-dev
on the command-line, nothing will be generated for the tests directories.
With that said, let’s set up the core class.
The Initial Backcast Class
First, I’m going to add a Backcast.php
file to the src
directory. I want to easily verify it’s been instantiated so to make this easy, it looks like this:
<?php
namespace Backcast;
class Backcast {
public function __construct() {
echo 'This class has been instantiated.';
}
}
And I’m going to update the bootstrap so it looks like this:
#!/usr/local/bin/php
<?php
namespace Backcast;
require 'vendor/autoload.php';
new Backcast();
Before doing anything else, I’m going to run $ composer update
on the command-line to make sure the autoloader is updated. Once done, I’ll try executing the main script again by calling $ ./backcast.php
.
Given the constructor’s statement above, I should see This class has been instantiated when the bootstrap is run (which I do).
Though I’m not ready to actually commit any code yet, I’ve got what I need to make sure PHPUnit can access the class and I can begin writing both unit tests and functions to pass the tests.
Verify PHPUnit Can Access the Class
Right now, I know there are going to be two ways to start Backcast
and that’s via the command-line and through unit tests.
If instantiated via the command-line, I can use the command-line arguments; otherwise, I’ll need to manually pass the string into the constructor of the class. This means I need to verify that if I’m using the command line, the code will:
- Validate the number of arguments,
- Pass the first argument that it identifies since that’s what it expects as the class,
- And provide a way to evaluate if the file exists.
The first two steps can be achieved in the bootstrap by checking the number of arguments before instantiating the class:
#!/usr/local/bin/php
<?php
namespace Backcast;
require 'vendor/autoload.php';
if ($argc !== 2) {
die("Please provide the path to the export file.");
}
new Backcast($argv[1]);
Everything above will be the same when using unit testing (or invoking it via code) except the constructor will accept its arguments as a string passed in from an invoking function.
This means the core functionality boils down to: Provide a way to evaluate if the file exists. So that will be my first unit test.
Testing The Code
First, I need to delete the SampleTest.php
that I created to ensure that PHPUnit was working. Then I’ll add BackcastTest.php
since I’ll be using a single class, for the time being.
Now I can start writing my tests.
1. Verify The File Exists
To test that the file exists, I’m going to write two unit tests that will test the following conditions:
- The file does not exist,
- The file does exist.
This won’t verify if they are valid or not, that will come later. My thinking is since no action can be taken until we have a file, then I need to test that first.
I need to make sure that the class throws an exception if no arguments are provided. This can be done with the following unit test:
public function testNoArgumentsException(): void
{
$this->expectException(ArgumentCountError::class);
$backcast = new Backcast();
}
Then I need to test if the file does not exist which I can do with an empty string:
public function testFileDoesNotExist() : void
{
$backcast = new Backcast('');
$this->assertFalse($backcast->exportFileExists());
}
And finally I need to test if the file exists which I can do using the same methodology:
public function testFileExists(): void
{
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->exportFileExists());
}
Now I need to write the class to satisfy the tests. The class should be simple enough, though. It needs to:
- accept a string,
- assign it to a class property,
- use a native PHP function to determine if the file exists,
- and return the result of the evaluation.
That can all be captured in a few lines of code.
<?php
namespace Backcast;
class Backcast {
private $xmlExportPath;
public function __construct( string $xmlExportPath ) {
$this->xmlExportPath = $xmlExportPath;
}
public function exportFileExists() : bool {
return file_exists( $this->xmlExportPath );
}
}
And now if I run PHPUnit I should see all green tests (technically, there should be three tests and three assertions).
And I’m good. At this point, I’ll commit the code to the repository and then move on to the next set of tests.
2. Verify The File Type
To verify that this is a valid file type, I just want to assert that I have an opml
file.
The OPML specification defines an outline as a hierarchical, ordered list of arbitrary elements. The specification is fairly open which makes it suitable for many types of list data.
Podcast export files are XML files in the format of opml
. It would stand to reason that checking the file suffix is one way to do it, and that’s true, but one could just as easily rename the suffix of, say, an image file to opml
and pass the test.
So I’m going to test that this has the proper suffix and that that the contents appear to be opml
.
First, the suffix test. This includes the following test:
public function testIsValidFileType(): vaoid
{
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->hasValidFileType());
}
And then it includes the following code:
public function hasValidFileType() : bool {
$fileParts = explode('.', $this->xmlExportPath);
if (!isset($fileParts[1])) {
return false;
}
return (
'opml' === strtolower($fileParts[1])
);
}
After that, I think I’ll use the SimpleXML
library that ships with PHP to traverse the the file itself. But first, I need to commit this test and function to the current feature branch.
3. Verify It’s Valid XML
To verify it’s valid XML, I want to verify the opml
tag is present and then I want to make sure that PHP doesn’t throw any errors whenever there is a problem loading the entire file.
Honestly, verifying that there’s an opml
file tag present in the XML file seems off but in order to keep and to keep shipping this thing, I’m going with classic string matching.
See the test:
public function testIsValidOpml() : void
{
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->containsOpmlTag());
}
And the code:
public function containsOpmlTag() : bool {
return (
false !==
strpos(
file_get_contents( $this->xmlExportPath ),
'<opml'
)
);
}
Next, I want to make sure that the XML file is valid. The thing is, I can’t know for sure if a DTD has been defined so I need a way to determine if the XML has any errors in it without comparing it to an actual DTD.
The best way I know to do this is to use DOMDocument
library (so this is a slight deviation from SimpleXML
at the moment) and determine if it throws any errors (from experience, I’m not sure if this is the best way to do it but if it’s sufficient, then why not?).
So here’s the test that will evaluate the validity of the file:
public function testIsValidOpml() : void
{
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->isValidOpml());
}
And here’s the function:
public function isValidOpml() : bool {
libxml_use_internal_errors(true);
$domDoc = new DOMDocument();
$domDoc->load($this->xmlExportPath);
return 0 === count( libxml_get_errors() );
}
The final thing I want to test is whether or not I see the attributes I expect to see.
4. Verify the Attributes Are What I Expect
Earlier in this article, I said that I wanted to make sure that I tested for the opml
tag, the type
attribute with an rss
value and the outline
tag with the xmlUrl
. At this point, I have the valid opml
tag so I can cross that off the list.
But let’s traverse the document and file all of the outline
tags that have a type
attribute and verify that they all have the rss
value. To do this, we can load up the document using SimpleXML
and get to work.
As I’ve been doing, I’m going to write the test first:
public function testHasProperOutlineTags() {
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->hasProperOutlineTags());
}
Then I’ll share the final version of the function that I’ve implemented (with comments included, if necessary) to show how it’s working):
public function hasProperOutlineTags() : bool {
$xmlDoc = simplexml_load_file( $this->xmlExportPath );
if ( 0 === count($xmlDoc->body->outline) ) {
return false;
}
foreach ( $xmlDoc->body->outline as $node ) {
if ( ! isset( $node->outline['type'] ) || 'rss' !== strtolower( $node->outline['type'] ) ) {
return false;
}
}
return true;
}
Since the xmlUrl
is another key in in the outline
tag, then this is a time where I can refactor the above function so it only loads the file once.
So I’m going to do that first. I’m also going to mark it as private as it’s a utility function inside of the class meant solely for loading a file and, at this point, we know the file should exist.
private function loadXmlExport() : SimpleXmlElement {
return simplexml_load_file( $this->xmlExportPath );
}
Next, I’ll make the obvious modification to update the code prior to this one so that it invokes this function (which you can see in the repository linked at the end of the article) and then I’m going to go through the process of verifying that xmlUrl
‘s are present for each of the outline
tags.
public function testHasProperXmlUrls() {
$path = __DIR__ . '/exports/sample.opml';
$backcast = new Backcast($path);
$this->assertTrue($backcast->hasProperXmlUrls());
}
And now the code:
public function hasProperXmlUrls() : bool {
$xmlDoc = $this->loadXmlExport();
if ( 0 === count($xmlDoc->body->outline) ) {
return false;
}
foreach ( $xmlDoc->body->outline as $node ) {
if ( ! isset( $node->outline['xmlUrl'] ) || ! filter_var( $node->outline['xmlUrl'], FILTER_VALIDATE_URL ) ) {
return false;
}
}
return true;
}
Above, I’m using one of my favorite utility features for evaluating URLs and that’s the filter_var
function. This is something that I don’t often seen used, at least in the context of WordPress in which I plan on eventually incorporating this code, so whenever I have the opportunity to use it I try to make sure I do so and share it.
Until Part 5
I didn’t get to the point where I was able to record the first episode of the podcast, but perhaps I can do that next week. Further, I’m thinking of making sure the number of outline
elements matches the same xmlUrl
and type
keys to ensure that every element has the minimum required values.
In any case, the repository is officially public and the develop
branch – which is the only branch that should be watched right now – is available. Note that if you’re reading this an open an issue or anything like that, I’ll likely close it because I’m not at a place where I’m ready to begin taking contributions.
But again, this is part of building it out in the open so we’ll see.
Scattered Thoughts
- Pocketcasts is a really nice app so much so I’m considering actually using it as my primary podcast player. More on that later, though.
- I’m no fan of god-classes or breaking object-oriented principles, but having to do this for the sake of getting something going quickly and that evaluates the logic necessary for the utility to run out weighs the work needed to dive into the whole OOP background that I’ll inevitably discuss. It’s a tradeoff, as with many things, and this is where I’m opting to make said trade.
- I really need to install
phpcs
at the project level. I’m tired of the mismatched standards I’ve been using (this is going to impact how the code reads in previous posts, sorry 🤷🏻♂️) but it’ll look good in the repository.
[ad_2]
Source link