Do you think it's a good idea to test your software?
Do you write unit tests or other automated verification for your code?
I think most of us do these days.
But a key question is, how do you know whether your tests efficiently verify your code?
And the standard answer is code coverage.
But there's a big difference between executing code,
which is what code coverage measures, and truly verifying it.
On this episode, we'll talk with Austin Bingham.
He created a mutation testing framework for Python
that goes beyond code coverage to actually perform this verification.
It's a fresh and powerful idea.
I hope you enjoy it.
This is Talk Python to Me, episode 63, recorded June 15, 2016.
Developers, developers, developers, developers.
I'm a developer in many senses of the word
because I make these applications, but I also use these verbs to make this music.
I construct it line by line, just like when I'm coding another software design.
In both cases, it's about design patterns.
Anyone can get the job done.
It's the execution that matters.
I have many interests.
Sometimes you can flip it.
Welcome to Talk Python to Me, a weekly podcast on Python,
the language, the libraries, the ecosystem, and the personalities.
This is your host, Michael Kennedy.
Follow me on Twitter, where I'm @mkennedy.
Keep up with the show and listen to past episodes at talkpython.fm,
and follow the show on Twitter via at Talk Python.
This episode is brought to you by Hired and SnapCI.
Thank them for supporting the show on Twitter via at Hired underscore HQ and at Snap underscore CI.
Hey, everyone.
We have an interesting deep dive into the world of Python testing and Python internals today.
Before we chat with Austin about mutation testing and his Python library called Cosmic Ray,
I have a few goodies to give away to a lucky couple of listeners.
First, Austin and his co-author Rob are giving away a copy of their book, The Python Apprentice,
as well as two free passes to their online Python course.
As always, just visit talkpython.fm and make sure you're a friend of the show to be eligible to win.
I'll pick three lucky winners next week.
Now, let's meet Austin.
Austin, welcome to the show.
Thanks, Mike. I'm really glad to be here.
Yeah, I'm super excited to share this mutation testing idea that you've sort of manifested in Python.
That's really cool.
We'll talk a lot about that.
Before we get into it, though, what's your story?
How did you get into Python and programming?
Well, how I got into programming was when I was quite young, I guess, around 10 years old.
We had a computer around the house, and it was an old IBM AT or something along those lines.
I forget exactly the model.
And it could be programmed in BASIC, and that's really caught my attention.
My parents got me some magazines and so forth that taught me how to do more complicated things than I could figure out on my own.
And it sort of took off from there.
Python, I have tried to figure out where I first started using Python.
And to be honest, I'm not entirely sure.
I think it was around in graduate school, though.
So this would have been in the late 90s.
Nice.
What did you study in grad school?
That was software engineering.
This was at University of Texas at Austin.
And we were doing all sorts of stuff related to artifact traceability in large-scale software systems.
And somewhere in there, Python showed up for a build system or something along those lines, and it really caught my attention.
And it sort of started there and grew and grew with my career.
It has shown up everywhere since then to larger and larger degrees.
So it's something that I've really enjoyed using for the past, I guess, 20 years at this point or so.
Yeah, that's definitely a while.
That's almost from the beginning, right?
Not quite, but it's pretty close.
Yeah, not quite the very beginnings, yes.
But it's been a long time, yeah.
Yeah, yeah.
Awesome.
So we were in Oslo together, Norway, last week with a bunch of other speakers and developers at the NDC, the Norwegian Developer Conference there.
And I would say that you and I were the guys carrying the Python flag, if you will, right?
Yes.
We were kind of the Python guys in the sea, among other types of folks, right?
Yes.
That's very true.
I mean, traditionally, Python doesn't have a large footprint at that conference.
And so you and I definitely were the diplomats, I think, the ambassadors for Python.
But I was surprised at how much interest there was in it among the other delegates.
A lot of people have some glancing experience with it, and they were, I think, interested to see or to learn more about it.
So I think it's a growing topic of interest, even at not traditionally Python-heavy conferences and venues.
Yeah.
Yeah, I've done Python talks at several conferences that were, I would say, decidedly not Python conferences.
And they've been received really, really well.
And I think it's just one more manifestation or one more piece of evidence that Python is really a growing ecosystem.
Yes.
Yeah, very much so.
And it's just gaining in popularity every year.
It's incredible.
Yeah, it is, I say this a lot on the show, but it's amazing to me that, like, the language grew at a pretty respectable but not insane growth rate for a really long time.
It kind of germinated and then, you know, just caught fire in the 2000s.
It's cool.
Yeah.
Now is the time for Python, yeah.
It is definitely the time.
So, well, the reason I brought up NDC is you had a really cool presentation there on this concept, which is a general programming concept.
It's available in Java and maybe some other languages.
I'm not sure.
But called mutation testing.
So I've done a lot of unit testing and other kinds of testing.
I've heard of genetic algorithms.
So maybe it's genetic algorithms.
I actually know, seeing the talk, I know that it's not necessarily.
Why don't you tell us what mutation testing is?
I thought it was really interesting, and I wanted to share it with the audience.
Sure, sure.
Mutation testing is, you can think of it in some sense as a test for your tests.
The main goal of mutation testing is to gauge the effectiveness of your existing tests.
So if you take the sort of theoretically perfect standpoint that you have a test suite that tests 100% of your functionality, at least in principle,
then mutation testing can tell you if your test suite actually does test your functionality.
You can find holes in your test system, and it can also help you find code in your code base that isn't tested, and maybe you can just be removed because it doesn't actually contribute to any real functionality.
So mutation testing, as you said, has nothing to do with genetic algorithms.
It doesn't try to search out failing test cases or something.
It's a very dumb algorithm.
It systematically makes small modifications.
It's kind of exhaustive, right?
Exactly.
It's an exhaustive, brute force search through a pretty large space to try to just trick your tests into passing a mutant.
I mean, the basic idea is that you make these very small changes to your code base and then run your test suite.
And if your test suite passes, then we say that the mutant test survived, and this is what you don't want.
This means that your test suite is incapable.
It doesn't have the fidelity for detecting the change you've made, which we consider an error.
Okay.
So before we get into that, though, you said something that I thought was interesting.
You said you have 100% code coverage, and yet your tests are not doing their thing.
So I think there's like layers of, or levels rather, of sort of verifying your test.
Like you're writing tests.
That's level one.
You have tests.
They exist.
It's an existing thing.
Yeah.
Step two or level two of enlightenment would be you have a significant amount of code coverage.
Because without code coverage, you can have like 1,000 tests, but they could all be about some small, useless part of your app.
And the important core section might actually not be tested.
But this supposes you're kind of, you're at level two enlightenment, right?
You have tests.
You have good, maybe not 100%, but you have pretty solid code coverage.
And now you want to say, is this actually, there's a difference between executing code and verifying code, right?
Right.
And this is a really important distinction that mutation testing gets to the heart of, which is, as you say, you could have 100% coverage in the sense that your test suite causes 100% of your instructions to be executed, however you define that instruction set.
But it doesn't tell you whether or not, traditional coverage doesn't tell you whether or not your tests are verifying the functionality.
So you could have a glaring defect in your program that your test suite is exercising, but not actually verifying.
And mutation testing goes to the next level and tries to tell you if your test suite is actually verifying functionality, if it's capable of detecting actual errors.
And if it's not, then we say that you need a more powerful test suite.
That's kind of the whole point of mutation testing.
It's this sort of adjunct to an existing test suite.
And just to add on that, I mean, this question of 100% coverage, no matter how you slice it, is a really hard thing for most projects to have.
In fact, most projects in the world don't have anything close to 100% coverage.
But mutation testing can still be useful, even with systems that don't have 100% coverage.
It just is going to throw up a lot of, initially at least throw up a lot of flags telling you that you have problems where you just don't have tests yet.
It's not a technique that can just be used on systems that have 100% coverage already.
I guess that's my point.
That's good, because that means it would have been excluded from quite a wide bit of code out there, right?
Yeah.
Nobody would be able to use it.
Yeah, that's the truth.
Yeah.
Right.
Okay.
So there's some really cool ideas here.
You're talking about a mutant.
This is the idea of changing your program, introducing some mutation.
It's just some, almost randomly find a spot and make a change and see what the effect is.
Because theoretically, you should be able to detect this change which broke something, presumably by changing it, right?
Yeah, that's exactly the case.
The mutations we're talking about, the modifications we're talking about are typically very, very small.
And so the kind of canonical example that it's always trotted out is replacing a relational operator.
So if I have some line of code that's, you know, X is less than one, I could change that.
The mutation would be to change that to X is greater than one, for example, and then make that one small change and then run the test suite again.
So these changes are very, very small, but the point is that the changes should all, in principle, be detectable by a sufficiently powerful test suite.
Okay.
That sounds like that would be true most of the time, but I think there might be some cases where it might not be detectable.
Before we get into that, though, I just want to clarify for the listeners, you're not the one doing the mutations, right?
Like, as a developer, that's not you.
Correct.
The whole point of a mutation testing tool is that it will do the hard work, the boring work of plowing through your code, finding the places that can potentially be modified, modifying them, and then running your test suite.
So in principle, you should be able to point the tool at your code, tell it what your test suite is, and then walk away for probably a very long time and then come back to get your results.
And, yeah, you don't – it takes away all the drudgery associated with that and actually gives you some really interesting results typically in the end.
Yeah, that is really cool.
So maybe we could think about, like, what does the mutation testing tell us?
Because sometimes you might make a change and your code will then fail the test, right?
So if you were testing that, I select a user and it's equal – the count of users I got back was one, and you change that to not equal, for example, your framework changes it to not equal.
Obviously, that test would fail, but it could also change things that I don't detect up.
I don't detect.
Yes, that's true.
You can have a result where your test suite passes a mutant, and then you go examine the code, and you realize that there's really no way to write a realistic test that would detect that change.
And this is a class of mutants that, if you read the literature, is called an equivalent mutant.
And an equivalent mutant is exactly that, a mutant that is functionally equivalent.
It's still a mutant.
It's been changed, but it's still functionally equivalent to the original program.
And for some reason or the other – and this is a very language-specific thing – but for some reason or the other, you simply cannot detect it.
And this is one of the really tricky, difficult aspects of mutation testing is ferreting out and somehow avoiding these equivalent mutants.
Yes.
Yeah, that's interesting.
So obviously, if we mutate it and then the test fail, that's an upvote for our test, right?
We've made a change to the code.
We reran the test.
The test said, your code has changed.
It's no longer good.
But then sometimes it might not come back.
Is it possible that if, like, you change some kind of, like, while loop condition, it could just go forever?
That's entirely possible.
And this is yet another class of complexities that we have to deal with in mutation testing.
These mutants that – the great example, the canonical example is what you said.
They go into an infinite loop.
So if I change, for example, one mutation would be to change a break to a continue.
And if you do that, then you typically create a situation where an infinite loop is very, very likely because you've taken a place where your code is in the exit condition and then where it wants to break.
And you said, no, don't break.
Continue the loop.
And it's going to stay in the exit condition and just kind of continue forever.
So that kind of mutant falls into the category that we call incompetent.
And I guess I should back up and say there are sort of three main categories for mutants.
After you've run your test suite, you have some results.
You talked about just a second ago where the test suite fails.
And we say that in that case, your test suite has killed the mutant.
Your test suite has failed, indicating that it knows that you've made a change.
The other broad category is that your mutant survives.
That is, your test suite passes.
And this is where we start to look for weaknesses in our test suite.
The third sort of smaller category is this category of incompetent mutants.
Most incompetent mutants fail immediately by throwing an exception or failing to compile or doing something along those lines,
something catastrophic that prevents them from actually even being run under the test suite.
And these, we still count these as killed.
These go in the checkbox category.
This is good.
But there are some incompetent mutants that do things like, you say, just run forever or maybe run for a very, very, very, very long time,
so long that we don't really want to try to see if they stop.
So this area is a difficult one.
It's one that you have to address on a practical level when you develop tools to do mutation testing, this problem of incompetence.
And, you know, you start looking into the theory of detecting incompetent mutants,
and you run smack into Alan Turing's famous proof about the halting problem,
saying that you cannot look at a program and determine a priori if it's going to stop running at some point in the future.
And that's the problem you face with incompetent mutants and mutation testing.
Yeah.
There's not even much reasoning about it because it's just a brute force method anyway.
Correct.
Yeah.
Yeah, that's a big challenge.
Can you give me some idea of, like, how frequent that category shows up?
Is that, like, 0.1%, 5%, 10%?
That's a tough question to answer on a global scale because, I mean, I obviously haven't run mutation testing on every program.
But in my experience, it's a relatively small amount.
I mean, less than 1% of mutations, far less than 1% of mutations become incompetent.
They're not a huge problem in practice because the strategies we use to deal with them are really simple,
which basically what we do is we timeout.
We establish, using one method or another, a timeout for your test suite.
And if it takes longer than the timeout, then we just count that as incompetent.
We say that mutant is in an infinite loop or in a huge loop, and we're going to say that it didn't get to run.
If you consider performance part of your feature set, maybe it's failed anyway, right?
Right.
Yeah, it's clearly problematic at that point.
Okay, interesting.
And you actually had two ways of timing out.
Like, you could just say, well, we're never going to run tests more than five minutes.
But you had a cool thing to do with baselines as well, right?
In the tools that we have right now for doing mutation testing in Python, the approach we take is to, well, as you said, one way is let the user provide a timeout.
They can just provide an absolute timeout, and we'll honor that.
Or we can run the test suite over unmutated code and time that and use that as a baseline and then let the user provide some multiplier, say, two or three.
And then if a mutant's test suite takes longer than n times the baseline timing, then we consider that an incompetent mutant and we kill it off.
So this is our really simple but generally very effective approach to dealing with the halting problem in practice.
Yeah, it's way easier than proving it, right?
Right, yeah.
So that would be really difficult to do.
Yeah, nice.
Okay, so we have obviously the case where the mutant is killed.
We have this incompetent mutant, which we kind of can't really deal with.
But then we have the more challenging case.
It's maybe the interesting case, you would say, where you've changed the code, you run the test, and the tests all still pass.
Right?
So there's a couple of conclusions you could draw from this, yeah?
Well, yeah.
If you've made a mutation and the tests still pass, then you have a couple of things to look into.
One possibility, the standard thing that happens in that case is that you just don't have enough tests.
You need more or better tests because you have some change that was undetected by your test suite.
The other very common, the less common but still quite common case is that you have code in your program that doesn't need to be there anymore.
It's extra code that doesn't contribute to any functionality.
So your test, in that case, your test suite is perfectly good because it's testing the things it needs to be testing,
the important functionality of your program.
But you've got bits of code that can be mutated but aren't being tested, so you should yank those bits of code out.
If you view code as a liability rather than something important to keep around, you can just get rid of it at that point.
The third possibility, and this is really a subcategory of the first, is that we come back to this notion of equivalent mutants.
Mutants that they have been changed.
Our test suite hasn't detected them, but there's no practical way to write a test for those.
And there's all sorts of interesting examples of these.
They're a bit difficult to describe, perhaps, purely without showing some code.
Speaking of showing, all the videos of the sessions, including yours and mine from NDC, will be online shortly.
And so as soon as they are online, I'll put a link to your presentation so people can go back and see it.
But yeah, it is tough to talk about code examples on audio, right?
Yeah, it's quite difficult.
But for Python, I think I could probably describe the Dunder main example.
Yeah, go for it.
So one equivalent mutant that, in retrospect, is quite obvious, but I hadn't really anticipated, is the standard idiom in Python of using Dunder name equals Dunder main to set up your main block when you're writing a program.
But of course, if you've got that in your program and you have any kind of code in that block that can be mutated, the mutation testing suite will mutate that code.
But of course, that block is never executed in a test because it's not accessible inside a test because Dunder name does not equal Dunder main in that case ever.
So you have this really interesting case where you have this whole body of code that's really important to your program in a way, but that cannot be tested and never will be tested.
So that's the flavor of at least some equivalent mutants.
But one of the joys of equivalent mutants is that when you find them, you have these aha moments almost every time because they're surprising, they're interesting, and they kind of make you scratch your head a little bit.
And so it's one of the, I guess you might say, strange joys of mutation testing.
Yeah, that does sound pretty interesting.
It definitely gives you some insight you probably wouldn't normally get.
This episode is brought to you by Hired.
Hired is a two-sided, curated marketplace that connects the world's knowledge workers to the best opportunities.
Each offer you receive has salary and equity presented right up front, and you can view the offers to accept or reject them before you even talk to the company.
Typically, candidates receive five or more offers within the first week, and there are no obligations ever.
Sounds awesome, doesn't it?
Well, did I mention the signing bonus?
Everyone who accepts a job from Hired gets a $1,000 signing bonus.
And as Talk Python listeners, it gets way sweeter.
Use the link Hired.com slash Talk Python to me, and Hired will double the signing bonus to $2,000.
Opportunities knocking.
Visit Hired.com slash Talk Python to me and answer the call.
One example that I was thinking of when you were talking about that category is like logging, right?
So maybe you've got some test, and it says, if this, then log this thing, else log that.
And, you know, would it really make sense to, like, write a test to detect what you're logging?
Right.
That's a really good point.
I mean, most, a large category of equivalent mutants are exactly of that flavor.
The changes caused by the mutation are things that you, in principle, could test for, but you never would because there's no reason to do it.
There may be no business reason to do it.
There may be just no practical reason to do it, depending on what values are driving your project.
And so you end up not ever writing tests for those.
And one of the challenges of writing mutation testing tools is allowing your user to specify, in some way, shape, or form, that mutations should not be performed on certain bodies of code for various reasons.
And this is something that all the tools that do mutation testing have to account for in some way, shape, or form.
Okay.
So I'm going to hold my question on, like, how you deal with that until we get to your framework.
Okay, good.
Because that is a really interesting problem, and I want to dig into it.
But before we do, like, could you give us some, you know, you talked about the sort of the basic changes, like, if you've got a less than, change that to a greater than.
There's a whole variety of different types.
There's, like, language agnostic changes.
There's changes you can make that affect object-oriented programming.
Can you give us, like, a sense of those?
Sure, yeah.
Yeah.
So this is actually one of the areas where there's active research into mutation testing.
There's not a huge group of people doing this research, but the research that is going on is, to a large degree, into which kinds of mutations should we be actually performing.
So you mentioned that there's some mutations that are language agnostic in the sense that they apply to almost all programming languages you can imagine.
And so a typical example would be something like replacing a constant.
If you found the constant 4 in your code, the mutation testing suite might change that to 5 or 19 or negative 6 or something like that.
So this is, you know, it's an obvious change.
I mean, it's a sort of blatant change.
It should be obviously testable.
And so it's ripe for that kind of thing.
But other examples include things like replacing arithmetic operators, removing unary or adding unary operators.
We talked earlier about relational operators replacing those.
And all of these sort of fall into – they're broadly applicable.
You could see them being applied in a functional language or an OO language or any other procedural language, whatever kind of language you happen to be working on.
But some research has looked into mutations that are specific to, for example, object-oriented languages.
Most – well, not Python, but a lot of object-oriented languages have access modifiers, for example, private, public, protected, and so forth.
And so one really clever and interesting mutation is to replace public with private or vice versa, to basically go in and mess with the access modifiers and see if that is detected by the test suite.
Often that results in something that can't compile.
In C++, for example, if you changed public to private, that would probably break compilation of many programs.
But changing private to public, it's hard to say.
That's actually very, very difficult to test for.
Other examples of OO-specific mutations would include, for example, changing base class order.
This is another one that can have really dramatic effects on what your program does.
Or in other cases, it can have absolutely no effect whatsoever.
So you could see how changing base class order in Python, for example, could have a completely undetectable – almost completely undetectable change to a program.
The only way to detect that would be if you had a test that was checking base class order, was checking the MRO for the class.
And, of course, nobody's going to write that test.
And I'm not advocating that anybody write that kind of test.
That's not the kind of code you want to write, that's for sure.
Right.
It's a complete waste of time.
So that's, I think, a fairly enlightening example of the kinds of problems you face doing mutation testing.
As elegant and interesting and straightforward as the approach sounds, there are these really difficult, thorny edge cases you have to deal with.
Other branches of research into, for example, mutations for functional languages.
In the classic example there, a lot of Haskell-like and F-sharp-like languages have pattern matching, for example, on their functions.
And changing the order of pattern matching is a common mutation you might perform on a language like that.
And that, again, is another area where sometimes changing the order makes a huge difference, and sometimes changing the order makes absolutely no difference.
And in those cases, I actually don't know how you would test for them because they're undetectable unless you have introspection and you have reflection capabilities.
You actually go in and do the kinds of tests I talked about a second ago that you would never write.
So it's a fascinating field to kind of dip your toe into, and the papers are pretty accessible if you want to read about these kinds of things as well.
It sounds to me like one of the major challenges that you're going to run into for almost any reasonable-sized program is that it's going to be really slow, right?
Because you're looking at basically every permutation of all the operators in, you know, inheritance, methods.
There's a crazy number of things in play here, right?
That's absolutely true.
I think the single biggest practical problem with mutation testing, the single biggest practical roadblock for using mutation testing is that it takes a long, long, long time to do.
If you consider the possibility of having, you know, dozens or, you know, 100 operators, you know, kinds of mutations that you might make in your code, and you have a large code base, you know, hundreds of thousands of lines of code is not uncommon in valuable systems.
Or even not very valuable systems, for that matter.
And then you consider the fact also that a test suite might take a considerable amount of time to run.
So you have this triply nested loop of the operators, the places those operators can be applied, and the amount of time it takes to run your test suite.
And you're talking about, you know, if you do the math, you can find on some systems that adds up to years.
I mean, literally, it's not something you can do on a practical basis for all your code in any way, shape, or form.
But there are some strategies that we can apply to try to deal with that.
The most basic strategy is simply to parallelize.
For all the problems we have with long run times and mutation testing, the saving grace is perhaps that it's embarrassingly parallel.
You can run each mutation slash test suite run in a completely separate process, all at the same time if you want to, and it won't affect the results.
So you could, in principle, go to Azure or Amazon and rent for five minutes, you know, 10,000 or 100,000 machines or whatever they'll let you get, run all your tests, and then be done with it.
But that's not something that is probably economically feasible for most people.
So other approaches that there are to dealing with this, well, there's not that many other approaches that I'm aware of.
But one is another form of baselining.
We talked earlier about baselining for your timeouts, you know, when to kill the test suite and call it incompetent.
Another kind of baseline that you can do is to run a full test suite with all your operators over all your code and get those results.
And then as you start making changes to your code base, only run the tests that, you know, work and exercise modified code, code that you've changed.
And that way you can drastically reduce the scope of the number of tests you need to run.
And that drastically reduces the number of operators that get applied, the amount of code space that can be potentially modified, and so forth.
Also, you can tell your mutation testing system to only mutate code that was modified.
So basically, we're analyzing deltas, analyzing our git diffs, so to speak, and saying only run the tests that we know could possibly have an impact or be impacted by the changes that were made.
And this is a heuristic approach to speeding things up because, of course, it's not watertight.
You could, of course, make changes to your code that influence the code paths that your tests are now exercising.
And if you purely use this kind of baselining for determining which tests to run and what to test, then you'll be missing things.
So you have to occasionally do, or at least in principle, you'll be missing things.
So you'll have to do occasional rebaselinings to make sure that you've kept up with all of your changes.
And it also assumes that you have some way of correlating your tests with lines of code.
So this is where mutation testing and traditional coverage analysis tools can come into play, where they can work together.
Because now you can say, okay, I take the coverage analysis information.
I know which tests exercise which lines of code.
I can couple that and kind of compare it to the deltas and determine which tests need to be run by the mutation testing suite.
It's basically like an inverted code coverage, right?
So if I look at this test, what part of my code in my real app changed or somehow was affected by running this test, right?
And so you could just focus on, say, like 10 lines of code or probably way more than that.
But focus it in on that area, right?
Exactly.
That's the point is drastically reduce the space.
And I think in principle you could get this down to where you could run things fast enough that you could do it on every commit or bundles of commits rather than once a week or something along those lines, which may or may not be desirable.
But it's an interesting goal from a tool developer's point of view.
Yeah, it's definitely an interesting goal.
Well, one of the things I was thinking of as you said this was, is this a thing that needs to run on, say, every check-in or every time you want to run your test?
Because if you have a good set of tests, hopefully your tests are actually catching your bugs.
And this feels to me like a validation of your tests rather than it seems like it could theoretically run less often and still be really valuable.
I think in practice you're right.
It doesn't need to run on every check-in.
But if you're working on a team that wants perfect code coverage, for example, and that requires, say you have a policy on a legacy code system that any change you make needs to be backed up by tests, which is a common thing to do with existing legacy systems that are trying to improve their lot in this world.
You might have that policy.
This is on every commit.
Whatever changes you've made need to be backed up by tests.
And this is a good way to verify that.
Not to verify just that you've made tests, but to verify that the tests you've created actually test the functionality correctly.
And so if you can make mutation testing fast enough, you could actually enforce that kind of constraint in a pretty strong way.
And that's an interesting thing.
Yeah, that is quite interesting because my experience is there's a massive difference among team members on their level of embracing testing and how much they run the test.
Like some people are really into it.
Some people only run it if there's something making them run it, basically.
Right.
Yeah, that's very true.
And so now you have a new stick to beat people around the head with if you have mutation testing in place.
Nice.
So let's bring this down to Python.
Let's make it concrete.
Let's talk about this thing called Cosmic Ray that you created.
Okay.
Yeah.
Cosmic Ray is, as you just hinted, it's a mutation testing tool for Python.
I should say it's not the first mutation testing tool for Python.
There were a few available when I started writing it, but none of them were work.
They didn't quite work the way I wanted or they were unmaintained.
And really, this was an interesting project in its own right.
So this started out almost as just a fun thing to do.
And it turned out to be a really fascinating project all around.
But Cosmic Ray is a system for searching through your Python code, finding places to mutate, making those mutations, and then running your test suite.
And it's a fairly young project.
And it has quite a bit of work left to be done on it.
But it has produced some results already.
So it's looking quite promising.
It's about a year and a half old, I think, at this point.
And really, it's only been used by me and a few sort of close, trusted friends.
But it's open source.
It's on GitHub.
And anybody who wants to try it or make contributions or give any feedback is more than welcome and, in fact, encouraged to go take a look at it.
Yeah.
Yeah, awesome.
And I'll be sure to link to the GitHub repo and things like that.
And it's on PyPI, of course, right?
Honestly, I'm not sure.
I think it is.
But I don't know the last time I pushed up a version to PyPI.
Let me see here.
Yes, it is.
It is on.
It's cosmic underscore ray on PyPI.
Woo.
Okay.
Saved.
I guess the interesting parts for a lot of people are going to be how cosmic ray works internally.
Yeah, absolutely.
There is some really amazing stuff in there.
Before we get into that, could you just really quickly, like, tell me, what do I need to do?
Like, if I've got some Python app with some tests, you know, I'm using pytest or something like that, like, what are my steps to apply this?
The steps are, well, pretty straightforward.
Identify the parts of your code that you want to mutation test.
Very often you'll have some part of your code that has a good test suite and is heavily, thoroughly tested and is central to the functioning and other parts that aren't.
And you can use cosmic ray to divvy out and slice and dice the parts you do and do not want to test.
So if you just want to take it for a spin, identify some module that you're interested in.
Because you want it to happen in a non-trivial, you know, in a short amount of time, right?
Well, that's one of the other reasons.
Yeah, you'll get more bang for your buck if you're just trying to test drive this than if you try to run it over.
You know, a 10,000.
If you want to run this over Django, forget it.
It's not going to work.
But I mean, not in any practical sense.
But if you want to run it over a single module in Django or, you know, some other package, then you'll have more luck.
That's been my experience, at least, with it so far.
But yeah, you'll need a test suite.
Right now, we only support unit tests, the standard library unit tests, and pytest as the test suites we support.
But there's a plug-in system for other testing systems.
If you feel you need one supported, they're pretty easy to add.
Point Cosmic Ray at your module and at your test suite.
And you'll pass it a few other parameters, you know, things having to do with timeouts and so forth.
And it will build up a work order, you know, basically the list of things it's going to do and put those in a little database.
And then you'll need to set up Celery.
Celery is a task distribution queue that runs on top of RabbitM queue by default.
And this, we use Celery to distribute work out to workers that actually do the mutation, run a test suite, and then send results back.
And so you'll have workers sitting on your Celery queue.
And then you'll tell Cosmic Ray to run the work order that it's built.
And it will start doling out work to these workers and collating the results back into the little database it's got.
And that's, I mean, that's the short version of what you need to do.
Once you have results back, then you start analyzing them and trying to figure out what Cosmic Ray is telling you.
Right, you look at those three categories, you decide what to ignore, what not to ignore.
Gone are the days of tweaking your server, merging your code, and just hoping it works in your production environment.
With SnapCI's cloud-based, hosted, continuous delivery tool,
You simply do a git push, and they auto-detect and run all the necessary tests through their multi-stage pipelines.
Something fails, you can even debug it directly in the browser.
With a one-click deployment that you can do from your desk or from 30,000 feet in the air,
Snap offers flexibility and ease of mind.
Imagine all the time you'll save.
Thanks, SnapCI, for sponsoring this episode by trying them for free at snap.ci.com.
And is there a way to flag it and say, this thing you've detected here, I want to ignore that?
Not yet.
And this is actually one of the big open areas for development, is how do we let users specify exceptions effectively?
How do we let them say, don't make this mutation on this line of code?
Or even more coarsely, don't make mutations on this line of code.
Because we need that kind of thing because of the problems of equivalent mutants and so forth that we have no real solution to.
Right now, there's a thought about the direction to take this in, how to deal with it.
If you look at tools like PyLint, they have great systems for putting essentially orders into comments in your code telling PyLint,
please don't apply rule such and such to this line of code.
We could probably apply the same kind of technique to Cosmic Ray.
But I'm not sure yet if that's better than having some extrinsic description of exceptions.
It's basically an open question.
And if anybody has ideas or wants to take a swing at it, this really is one of the big things that we need to sort out soon.
Let's look inside.
Basically, you point Cosmic Ray at your module and you say, go shred this thing.
And for every shred that you create, go run the unit test, right?
That's exactly right.
Walk us through the internals there.
There's some interesting stuff you're doing.
Well, at the core of all of this is the standard library module AST.
AST is short for, what's it, acronym for Abstract Syntax Tree.
An Abstract Syntax Tree is just a programmatic structure defining a program, the syntax in your source code.
When Python parses your source code, it produces an abstract syntax tree.
And then you can access this looking at the different nodes in the tree, you know, looking at the different parts of your program.
And not just look at them, but you can also change them.
So what AST allows us to do in Cosmic Ray is load up your source code.
We literally read your source code from your .py file.
And we pass it into a parse function, which parses the source code into the abstract syntax tree.
And then AST has other components, which allow us to walk down that tree.
And if we want to make changes, the details of exactly how it does that, it might be difficult to talk about operators and things like that in too great of detail.
Yeah.
So, well, basically you get this abstract syntax tree and then you start applying your transformations to it, right?
Your mutations, if you will.
Yeah.
Well, that's the fundamental idea, yes.
So you have the AST and you find a place that you want to make a modification and then you make a modification to it.
And there's support in the AST module for doing that kind of work.
Once you've modified the AST, you then need to get it, you need to make it available to your test suite.
You need to make it importable.
And that's a whole other kind of second level trick.
Yeah, because there's one thing to say, hey, Python, run this module.
It's another to load up an individual AST and then turn that into executable things, right?
Exactly.
Everything it depends upon and so on.
That was sort of the second big phase of work in building Cosmic Ray was figuring out how to do that.
So once you have an AST, a modified AST, you can pass that to the built-in compile function.
And that spits out what's called a code object.
And that's this kind of thing that modules can use, so to speak, that we can execute to populate a module.
So figuring out how to make that available, make your modified AST available through standard import was a big goal of Cosmic Ray.
We didn't want people to have to modify their test suites to do mutation tests.
We wanted the test suites to just naturally say import, you know, import of the module and get the right one.
So we had to do a lot of investigation into how Python does this.
And at the core, there's three main moving parts to how Python does imports, how it lets you control imports.
The first thing is what's called a finder.
And a finder is an object that's responsible, a class typically, but a function or a class that's responsible for telling Python that it knows how to load a module given that module's name.
So Python will ask the finder, I've been asked to import foo.
Do you know how to do anything with foo?
And a finder can say yes or no.
If a finder does know how to load something, it returns what's called a loader.
And the loader is then responsible for populating essentially the shell of a module.
So Python will make the empty shell of the module, pass it to the loader, and say, okay, now you populate this with the names, the functions, the name bindings, the constants, all that kind of stuff that come from the module that you're supposed to be loading for me.
What we do is in Cosmic Ray, we have our own custom finder.
And that finder is given the modified AST, and it's told the name of the module.
And if it's then asked by Python, do you know how to load that module?
It'll say yes.
And then it hands back a loader.
We have a custom loader, which also has this AST.
And the custom loader is able to compile the AST, and then use that compiled AST to populate the shell module.
And then that shell module is passed back to Python, and it gets naturally imported so that everybody can use it.
I guess the last sort of moving part in this whole system is something called sys.metapath.
If you import sys, you'll see it has an attribute called metapath.
Metapath is just a list of finders.
And when Python wants to import something, and some experts might tell me that I'm a little bit wrong with the details, but this is effectively correct.
Python marches down metapath, asking each finder in order, do you know how to load this name?
And the first finder that responds is the one that wins.
So what we do is we take our custom finder, we populate it with its AST and its name, and we stick it at the front of metapath inside our worker processes.
And these worker processes then are able to hijack the import system, in a sense, and put these mutated ASTs directly into place so that nobody has to know they're there, but they get imported naturally by whoever wants to use them.
So that's the long and the short, I guess, of how we stick mutated ASTs into Python programs.
Yeah, you really had to dig deep down inside the guts of Python.
You had to take the red pill, not the blue pill, right?
Yeah, there's a lot of pet archaeology and stuff here to get to the bottom of this.
But at the end, it's very elegant and powerful.
One of the joys of this project was learning all this stuff that I may never apply again, but I feel like I've reached the next level of my Python expertise, in a sense.
Yeah, that's really cool.
But it's awesome because you don't change your code to make this happen, right?
It adapts to what it has to do to basically take over.
Exactly.
We work, Cosmic Ray works at a deep enough level that neither your test code nor your code under test needs to be modified to use Cosmic Ray.
It should work transparently in all ways.
Yeah, that was a big goal of the project.
You talked about Celery.
Celery is really awesome.
There's a couple of other really cool projects that you're kind of built upon.
One of them was this thing called TinyDB.
Yeah, TinyDB.
Well, it is what its name says.
It's a tiny database.
It's a little embedded file-oriented JSON database that you can import into your Python program and use with basically no configuration.
So it was exactly what I was looking for when I was looking for a database for Cosmic Ray.
We use the database for basically keeping track of the work order I described earlier.
The first thing you do in a mutation testing run is figure out what it is you're going to do and write all that down.
We write that into the database.
And then as the results arrive back via Celery, we stick the results back into this database.
So TinyDB is something that's worked out really well for us so far.
And it was, as I said, super easy to use, and it's stuck around so far.
I have a feeling that it's going to end up being a bottleneck in larger projects.
But that's a gut feeling.
I don't have any evidence to indicate that.
But if it has to be replaced, then we'll start looking at something like SQLite,
or maybe we'll give the user the power to specify MongoDB or whatever they want.
But TinyDB is really worth looking at, I think, if you don't have really sophisticated needs in a database
and you want something that's just file-oriented.
It's a really beautiful little program that worked out of the box with really no reading on my part whatsoever.
That's lovely.
I really like to use SQLite and SQLAlchemy together.
And those work really well in sort of an equivalent way.
But I'm a huge fan of the document databases.
One of the big selling points, what made me stick with TinyDB, is that it literally is a JSON file.
I can open it up in Emacs and just look at it.
And I don't have to have any extra tools to examine its contents.
I think that that JSON nature is what's going to be its downfall.
That's what makes me think it's not going to last that long for this project.
But that's been a real selling point, is I can run my tests as I'm testing CosmicWrite,
which, as you might imagine, is a real challenge, and then see what's in the database really, really easily.
And so my cycle time has been pretty high by using TinyDB.
Oh, yeah.
That's cool.
And it's 100% Python, according to GitHub.
That sounds right.
Yeah.
I don't remember any compilation happening when I used it.
Yeah.
Nice.
Yeah, it has 1,000 stars.
So it's doing pretty well.
I definitely want to check it out.
The other one was DocOpt.
Yeah.
DocOpt is one of my current favorite packages for not just Python, but for lots and lots of languages.
DocOpt is a tool for building command line parsers.
But unlike things like ArtParse or the other sort of standard tools for doing this, it takes a kind of a backwards approach.
You provide it with a string, which is the POSIX standard help output that you would get from any program.
You know, saying usage colon program name, blah, blah, option names and all that kind of stuff.
And the text information somebody gets when they type, you know, program dash H.
You give that string to Cosmic Ray, and from that it generates a parser that can then parse command line arguments.
So you never have to think really hard about, you know, building up these parser objects yourself.
Everything is done magically, and all you have to do is think about how your pretty help message is going to look.
Which you've got to write anyway.
Which, yeah, you either have to write or have to get generated by some other tool.
This has the neat effect that embedded in your code somewhere is your full help message that is great documentation, not just for your users, but also other programmers looking at your code.
It really, it solves a really annoying problem that everybody and every programmer in the world has, which is writing parsers for command line arguments.
And it does it in a really slick way.
And one of the interesting things is that, I didn't know this until I looked at DocOpt, is that there actually is a POSIX standard for these help messages.
So it can rely on an actual existing standard for defining these things, which is really cool.
That is cool.
Actually, that's the first way that I had heard of that there was a standard was by learning about DocOpt.
Like, wait, there's a standard for the help message?
Interesting.
I highly recommend that anybody who has to write command line tools and who hasn't tried DocOpt, take a look at it.
It's really addictive.
And you can produce really, really powerful command line parsers.
You know, things like you had with Git, you know, sub-command-based tools and stuff.
I guess the other interesting thing about DocOpt is that while it was originally written in Python, the canonical implementation is Python.
It exists now for something like 30 languages.
So if you're a sometime C# developer, sometime Java developer, sometime whatever developer, you can continue using DocOpt in those languages as well.
It's a neat project from that point of view, something that you don't see a lot of.
Definitely means the idea of it resonated super well, right?
Yeah.
Yeah, it did.
Okay, so we're getting kind of near the end of the show, and I wanted to ask you, you know, you have a company called 60 North, right?
That's correct, yes.
Yeah.
You and Robert Spolscher, is that right?
That's right, yes.
You guys work together.
Yeah, you guys are up in Norway, which is why I ran into you in Oslo.
That's right.
That's awesome.
Although we also seem to run into each other in London.
Yeah.
Yeah, what do you guys do there?
Well, 60 North, we're not terribly pigeonholed, but we do do a lot of Python work.
We do consulting, training, some development of our own as well.
We've made some courses for Pluralsight.
So if you go to Pluralsight and you look for the Python training courses, we have the Python Fundamentals as our first course.
And Python Beyond the Basics, which is sort of the next step, intermediate level, is there.
And we're working on a third one, which is Advanced Python, I think, is the official name.
And that will be out by the end of the year, hopefully.
Okay, yeah.
You and I, we're both very passionate about online courses.
Tell me, what's in your intermediate and your advanced courses?
Oh, I'm going to stretch my brain to remember exactly the contents of those courses.
But I know the intermediate course, we start getting into things like decorators, class properties, some of the details of classes beyond just functions and methods.
Here's how you define a class and add fields to it.
Right, yeah.
Getting beyond that.
A couple of things that are beyond the basics.
You'd be surprised at how many things there are that go into the basic course that are really, really basic.
I mean, the course is quite long and doesn't really scratch the surface of Python.
So anything like, like I mentioned, decorators or...
Probably Lambda, Lambda expression type thing.
I think Lambda's in there, context managers, implementing a lot of the Dunder magic methods.
That kind of stuff is in the intermediate.
And then the advanced classes where you start to get into things like what we talked about earlier, finders and loaders.
Or you start getting into meta classes.
Things that we classify to a degree as things you might do once a year instead of things that you do every day as a professional Python programmer.
I mean, finders and loaders, I programmed Python for 20 years and never used it.
But it's an interesting and important part of the language.
So it needs to be in there somewhere.
Yeah, and once you understand it, maybe you don't use it often, but knowing the mechanics helps you understand a lot of things often at that level.
Yeah, and you know you have that in your pocket.
And so that might be the most elegant solution for some particular problem you face rather than some horrible hack you would have to come up with otherwise.
So the advanced stuff is for people who are using Python a lot and need to find the best solutions and really understand the inner workings of the Python runtime.
Yeah, cool.
So if you guys have a Pluralsight subscription, go over there and type in Python in the search box and you'll find Austin.
Yeah.
Nice.
And you also wrote some books, too.
We do have some books.
Yeah, we have – the books are based largely on the same material as the Pluralsight courses.
And the first one is I think 90% done now.
It's on LeanPub.
It's called The Python Apprentice.
The second and third books, The Python Journeyman and The Python Master, are in the works and will be published probably not this year but soon.
And since they're on LeanPub, you can get the early version and we'll keep sending you updates as we make updates to the books.
But if you prefer books, these are available as, I think, PDFs and Mobi's and EPUBs on the LeanPub site.
Nice.
And that's self-publishing, right?
That is self-publishing, yes.
Yeah, very cool.
I'm a big fan of self-publishing.
So I like to see when people are succeeding at that.
That's great.
Yeah, so I'll be sure to link to all those things in the show notes as well.
Okay.
That'd be great.
Yeah, absolutely.
Very interesting.
Definitely cool.
Two more questions before I let you go.
What's your favorite PyPI package?
I saw the other day there's over 80,000 distinct packages out there.
That's an insane number.
That's probably why people like PyPI.
But there's got to be something that you've had exposure to that you want to share, like, oh, you should check this out.
Well, this is going to feel like a bit of a cheat, but DocOpt.
DocOpt is one that I, once I learned about it, I started using it on almost every project I use.
But I know that it's not that well-known, at least not as well-known as I think it should be.
So I'll just put a second vote in for DocOpt.
For my money, that's the tool I keep going back to in PyPI every time.
And it should be more widely known and more widely used because it's awesome.
Yeah, that's awesome.
I'll go ahead and throw one for Cosmic Ray in there for you because that's pretty awesome and very interesting to check out.
Thanks.
And then you mentioned Emacs earlier, but if you're going to write some Python code, what do you typically open up?
Well, the short answer is Emacs.
I've been using Emacs for almost as long as I've been using Python, I think.
And it's in my fingers to a degree.
If I know that I'm working on just a dedicated Python project, then PyCharm is a wonderful IDE.
And it's got a lot of powers that Emacs doesn't have when it comes to working with Python.
Well, that Emacs doesn't have yet, I should say.
But no, it's really great for just pure Python editing.
I guess the reason I stick with Emacs is, well, stubbornness to a degree.
I just, I'm old and don't want to change.
But I'm also very often working on multiple languages at the same time in any given project.
You know, everything from JavaScript to Python to, you know, L to whatever happens to be part of that project.
And I find that Emacs makes it easier for me to do that.
Or at least it has, it's not, it's the best of, it's the best of breed for that kind of work from what I can tell.
And honestly, Python as an Emacs IDE is pretty good.
You can do all sorts of fancy stuff in there if you want to spend the time to configure it.
And if you use a package like, or a canned Emacs configuration like SpaceMax, you'll find that you get pretty sophisticated support for things like completion right out of the box.
You know, you get Jedi support and things like that.
So it's, I know that I try not to recommend Emacs to new people, to people getting new to Python because that adds a whole level of complexity.
But Emacs as a way of life is, it's an interesting place to be.
So you could do a lot worse as a programmer.
So any final call to actions for all the listeners while you got the mic?
Any more calls to action?
Are you looking for contributors to your project?
Certainly.
Sounded like you were.
Certainly Cosmic Ray could use some people who are willing to put in some work.
We have, of course, the GitHub issues page where I keep track not just of defects, but also of, you know, the higher level issues that need to be done.
I mentioned earlier that we have this pressing need for being able to embed exceptions and processing instructions in our code so that Cosmic Ray can not do certain kinds of mutations.
And that's a big project that somebody might be able to take on.
We have, I guess, the two other big topics I could think of are support for different kinds of modules.
Right now, Cosmic Ray can only work against modules that are written in pure Python codes of .py files.
But, of course, there are plenty of other exotic kinds of modules out there.
So Cosmic Ray needs to either gracefully skip over those other kinds or learn how to process those.
And there's no support for that right now.
And that's a big limiting factor.
And the other is, this is more of a researchy thing, but the integration with coverage testing that I talked about earlier.
Being able to take output from, say, coverage.py and use that to determine how we can narrow down the scope of Cosmic Ray mutation testing runs and make it a more practical tool.
But, really, it's, you know, go to the issues page on GitHub and look and you'll see the nature of the things that are going on.
Yeah, that would be my call to action, I guess, for Cosmic Ray.
All right, fantastic.
I'll put the link to GitHub repo in the show notes.
So, Austin, it's been really fun to talk about this idea of mutation testing.
I think it's a really interesting evolution, if you will, of all the testing tools, right?
I can see a place when this algorithm gets tuned and, like, the various optimizations you talked about get in there that this could be a big part of day-to-day work.
It's cool.
Cool.
I'm glad you think that.
And thanks for having me on the show to talk about it.
It's something I really enjoy talking about in public.
So, yeah.
Yeah, you bet.
You bet.
Thanks for being on the show.
And it was great to see you last week.
Take care.
It was great seeing you last week.
All right.
Bye, Mike.
This has been another episode of Talk Python to Me.
Today's guest was Austin Bingham.
And this episode has been sponsored by Hired and SnapCI.
Thank you guys for supporting the show.
Hired wants to help you find your next big thing.
Visit Hired.com slash Talk Python to me to get five or more offers with salary and equity presented right up front and a special listener signing bonus of $2,000.
SnapCI is modern, continuous integration and delivery.
Build, test, and deploy your code directly from GitHub.
All in your browser with debugging, Docker, and parallels included.
Try them for free at Snap.CI slash Talk Python.
Are you or a colleague trying to learn Python?
Have you tried books and videos that left you bored by just covering topics point by point?
Well, check out my online course, Python Jumpstart by Building 10 Apps at talkpython.fm/course to experience a more engaging way to learn Python.
You can find the links from this episode at talkpython.fm/episodes slash show slash 63.
Be sure to subscribe to the show.
Open your favorite podcatcher and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, Google Play feed at /play, and direct RSS feed at /rss on talkpython.fm.
Our theme music is Developers, Developers, Developers by Corey Smith, who goes by Smix.
You can hear the entire song at talkpython.fm/music.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Smix, let's get out of here.
I'll see you next time.
Bye.