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 we should make pass (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.