Hello, in this sequence we'll look at the SUnit test framework in Pharo,
which allow us to write unit tests, as you're going to see.
A test framework is used to support Agile development.
The idea is to do incremental development and to test
your code regularly to check
that in modifying it we haven't broken a property or invariant in the code.
We want to be prepared for changes.
We write tests then we modify our codes
then we can re-execute the tests
to check we haven't broken or modified anything we shouldn't have.
Automated tests are very important in supporting this kind of programming.
The SUnit framework is a special framework for writing tests.
It makes them easier to write. It's in 4 classes, it's very simple.
It was originally developed by Kent Beck,
and has inspired numerous tests in other languages,
like JUnit, for example.
So, what is a test?
There are 3 steps to writing a test:
Step one, we create a context, for example, an empty set.
Step two, I create a stimulus,
here, so I try to insert an element twice
in the set I've just created,
and step three, I test the result that I get,
which means that I wait until the set only contains one element.
A set object can only contain an element once.
It can't contain it twice.
Then I test the result, hoping the variant isn't broken.
Here's an example. To write a test...
I have to write a TestCase subclass. I'll call this one "SetTestCase".
It's to test the sets. I define the method, "testAdd".
All the methods begin with "test".
Then I set up the context,
I create an instance of the Set class, which is empty,
I add 2 elements, the same element twice...
I try and add the same element twice
to the bundle, so 5 twice, here and here.
Then I test it, I've "check" here,
I use "assert" to do this,
where I check that the size of my set
is 1, that I've succeeded in only adding the element once.
I can start the test thanks to this...
Thanks to this expression, so my test will reveal
if the variance, if "assert", the expression passed here, is true.
All the methods that begin
with the string "test" represent a test,
and will be automatically executed by the test runner tool.
We'll see that all the results, all the executions of the test method
produce a result. All these results are collected,
and they're collected within a class instance object, TestResult.
I'll give you another example.
In this example, we've the test method, its name begins with "test"
in lowercase, and this is the name of this method,
"AdjacentRunsWithEquals AttributesAreMerged"
So we've the context here, we've created an object.
Here we've a stimulus.
We've tried to send the message "addLast times"
to this object 3 times, the 1st time with
the settings here, and a 2nd time, the same settings
the 2nd and 3rd time.
We test here, it's the check,
that this element is size 2. All right?
We weren't able to add the same element several times adjacently.
In the execution of a test, several scenarios can occur.
One scenario is what we call a "failure", meaning that
one of the assertions, a variant we thought was true,
which should be true is false, in which case
the test which contains "failure"
is an anticipated problem.
We expected that, potentially, this error would be present.
After that, an error is a condition we didn't check for.
It's something that happens, an exception that's raised,
which we didn't expect when writing the test.
They're 2 very different cases.
What do we do in a test when we want to check
that a bit of code raises an exception?
For example, I want to check that "set new remove: 1"
this bit of code, will raise the exception "NotFound".
If I do "Set new", it's an empty set.
I'm trying to take an element from an empty set.
It doesn't make sense. It will raise the exception "NotFound"
and in my test I use "should raise".
I pass a block and in my evaluation of the block,
if an exception is raised,
and the exception is NotFound, the test will be green, will be OK.
I can also test the reverse,
that a bit of code doesn't raise an exception.
Here I use the method "self shouldn't raise".
So this bit of code shouldn't raise the exception "NotFound".
We might, when writing lots of tests,
realise that there are duplicates
when writing the context of the test.
For example, here I've written another test
for the sets, a "testOccurrences".
We see that here, in the context, I'm going to recreate a new empty set.
Each time I write a test, I open a set,
and each time I'll do "Set new" in the context.
We'd like not to repeat this line every time in all of our tests.
To not have to repeat them, to be able to factor it out
somewhere else, we have a solution.
The SUnit solution is to use the method "setUp"
to factor out all the initializations before execution of a test.
So what actually happens is,
at the moment a test is executed, just before a test,
therefore a method starting with the test string, is executed,
we'll trigger execution of the method "setUp",
and specify the context.
During the test we'll use the stimuli
and the check, the assertions,
and at the end of the execution of the test
whether it fails or not,
we'll execute the method "tearDown",
which will allow us to clean up
all the resources that should be released.
If we look at the execution of several test methods, it's easy,
we'll have the execution of "setUp"
the first test method executed here,
the execution of "tearDown" to clean, a new execution of "setUp"
the execution of a new test, "tearDown", "setUp",
the execution of a test and "tearDown'.
This allows us to factorize implementation of the context,
and clean up resources in two methods, "setUp" and "tearDown".
What does it look like?
In our example, if I take "SetTestCase"
I can put in place "define setUp method",
in which I write, "empty :=Set new",
so "empty" becomes an instance variable of "SetTestCase",
and then in my test, in my test method,
I can directly use the instance variable "empty",
which was correctly initialized, because before execution
of "testOccurence" the method "setUp" was executed.
If we look at the organisation of the classes
within the core of SUnit, as I said, there are only 4 classes:
so one test case, which is nothing more nor less
than a test that verifies that certain conditions,
are true in a given context, so one test case has
one "setUp" method one "tearDown" method
and then a group of test methods.
We write a "new" all the time, the subclasses of the test case.
These test cases are combined
in a "TestSuite",
and we can launch the execution of a complete suite.
When we launch the execution of a suite we get a result,
and this result is an instance of "testResult" here,
which tells us how many tests have passed,
how many tests have been executed, how many have potentially
met with failures and errors.
We also have the notion of "TestResource"
which allows us to define the TestResources for a whole suite.
A TestCase, as I said, represents one test.
It's one method starting with "test" defined in a subclass, TestCase.
A TestSuite is a group of tests.
It's all the TestCase methods defined in one or several classes.
And a TestResult will be a result of several test executions.
A TestResource is an object that will enable
the initialization of a group of resources,
which are costly to initialize in normal time,
and which we only want to initialize once for a group of tests.
We set up a TestResource, initialize it once and execute lots of tests,
and then we can release it at the end.
What you should know
is how to write tests. Writing tests is extremely simple,
you just have to write a subclass of the TestCase class,
define the methods in it which begin with "test",
and then set up a context inside it,
send the stimuli and test the assertions, which should be true.
We'll reuse the contexts,
so you can reuse the contexts through several test methods,
by factorizing them in a SetUp method for example.
To summarize, in this sequence we've seen the SUnit test framework,
which is extremely simple to use,
and extremely efficient for setting up Agile developments.
I strongly urge you to use them, defining tests is very easy.
The big advantage is that when you've created one test
you can run it a million times and it's really handy for making sure
that your code still works, even if you've changed things,
and edge effects have occured, you can detect them
if you've been up-to-date enough in the tests you've written.
You can go further in creating tests by using dot frameworks,
typically Mock frameworks like BabyMock, etc...
to have different styles of test and test writing.
I encourage you to use and create a lot of tests in your program.