Getting the fetcher
code up and running seemed like a fairly big job.
After a big job, I usually take on something smaller and easier, aiming for that quick win.
Looking at the Proof of Concept code a quick win would be validating the URL.
Here’s the code:
try {
if (redirected) {
const hrefToUrl = new URL(location);
console.log(`hr`, hrefToUrl);
}
} catch (e) {}
Code language: TypeScript (typescript)
I mean, it’s bad.
Swallowing exceptions and silently doing nothing is rarely a smart move. But hey, proof of concept, right?
Yeah, it’s a poor excuse.
Anyway, let’s make this better.
URL Validator Unit Tests
JavaScript provides us with the URL
object.
Assuming we have a valid URL as a string, we can use this when creating a new URL(...
);.
This object then gives us a bunch of properties and methods for working with URLs in a nice way. We can get the hostname
, or the protocol
, or the path
. Also we could set up a query string (?hello=world
), or mess around with the fragment (#things
).
But, if you don’t provide a valid URL when instantiating a new URL
, then you get an error:
So we can use this to our advantage.
Providing we wrap the call to create a new instance of a URL object in a try
/ catch
block, then we can return a boolean to say whether the given URL was valid or not.
Here are some unit tests to cover the various cases I can think of:
import { isValidUrl } from "./is-valid-url";
describe("isValidUrl", () => {
[
[null, false],
["", false],
["null", false],
["http://", false],
["https://", false],
["https://example.com", true],
["https://www.example.com", true],
[
"https://learn.microsoft.com/dotnet/core/tutorials/top-level-templates",
true,
],
].forEach(([given, expected]) => {
test(`given: ${given}, expected: ${expected}`, () => {
expect(isValidUrl(given as string | null)).toEqual(expected);
});
});
});
Code language: TypeScript (typescript)
We’re throwing a variety of weirdness at this.
The structure I’m using here is [input, expectedOutcome]
.
Taking the first entry in our array we therefore have:
- input:
null
- expected outcome:
false
If the given value is null
then no, this is not going to be a valid URL.
Likewise, an empty string on line 6 is also invalid.
Things might look initially correct, such as on lines 8 & 9, but they don’t make a full URL, so are also invalid.
Everything from line 10 onwards should be valid.
A Basic isValidUrl
Implementation
The code here is really straightforward:
export const isValidUrl = (url: string | null) => {
if (!url) return false;
try {
new URL(url);
return true;
} catch (e) {
return false;
}
};
Code language: TypeScript (typescript)
We have to allow for null
as it’s possible that there will be no “next location” in our wider implementation.
Typing given
The one quirk to this otherwise fairly standard JavaScript-y looking TypeScript code is the type assertion against given
on line 18:
expect(isValidUrl(given as string | null)).toEqual(expected);
Code language: TypeScript (typescript)
And if we narrow this down further:
isValidUrl(given as string | null)
Code language: TypeScript (typescript)
If we don’t provide any type information then TypeScript will get confused:
And you may be wondering why the error mentions that given
could be a boolean
when we only seem to have string
and null
values here.
Being completely honest, that is something I didn’t even notice until I got the error.
The issue here, to the very best of my knowledge, is that TypeScript has been unable to infer that my outer array contains several tuples. It would appear that TypeScript has only been able to infer that my individual test cases are arrays of string
and boolean
and null
values.
There are a couple of ways to fix this.
Type The Tuple
One way is to provide a more explicit type to the array:
(
[
[null, false],
["", false],
["null", false],
["http://", false],
["https://", false],
["https://example.com", true],
["https://www.example.com", true],
[
"https://learn.microsoft.com/dotnet/core/tutorials/top-level-templates",
true,
],
] as [null | string, boolean][]
).forEach(([given, expected]) => {
test(`given: ${given}, expected: ${expected}`, () => {
expect(isValidUrl(given)).toEqual(expected);
});
});
Code language: TypeScript (typescript)
Here we describe that the type of data in our array will have the shape of null | string
for the first element, and the second element will always be a boolean
.
Now given
is happily narrowed to either null | string
, and expected
will only ever be a boolean
.
Use A Type Cast
Another way is to provide an explicit type cast (or type assertion) against given
:
expect(isValidUrl(given as string | null)).toEqual(expected);
Code language: JavaScript (javascript)
That’s the implementation I used above.
This is significantly looser.
Here, expected
could still be of type string | boolean | null
, but Jest doesn’t care. So long as the outcome matches the expected value, all is fine.
However, we have kinda glossed over that really we don’t quite know what given
may be when we run this. But we’re fairly sure it will be one of the two types we provided.
That said, this won’t magically change the run time value of given
.
If given
were some other value, such as a boolean
or another array, or whatever, this wouldn’t be magically changed at run time. That might blow things up. In our specific case, it’s probably fine.
Not great.
However, both approaches solve the problem.
My Preference
Where possible, I prefer the stricter variant.
That means, for me, I prefer the typed Tuple approach.
I think both are a little tricky to read in their own ways.
One way to slightly improve things for the typed tuple approach might be to name the type:
Ultimately just because this is test code doesn’t mean we should give it any less love than we would for implementation code. Heck, I usually spend longer in the tests than on the code itself.
But as ever, personal preference.