Makers Week 1: Surfing in the coding ocean but I managed to catch some fish
Test-Driven Programming, Debugging, Object-oriented Programming…

London, Shoreditch
The first week at Makers passed so quickly before I even realise it. There is such an information explosion and everyday I was thrown with tons of new stuff.
— Yet, exciting!
However, good thing is that I managed to catch a few big fish in chaos :
- Object-oriented programming: A few high-level design concepts
- Test-driven programming: Guidelines, RSPEC (how to set-up and basic syntax), Test doubles (such a powerful tool!)
- Debugging: Common procedure to debug an error
These 3 points are equally important, because you can almost say that they lay the first brick for your coding career. I do feel, however, understanding OOP (Object-oriented programming) will improve my learning experience with testing, so that’s where I started.
I really like reading the high-level design concepts in OOP. Encapsulation, Single Responsibility Principle, Polymorphism. It helps me to understand the code from others and to design my code before I enter into granular levels. For example. the Single Responsibility Principle helps me to avoid stuffing all the functionality into one single method, which I enjoyed doing previously. For now, I know that I only have a shadow understanding of these design concepts, but it really good to get exposed at early stage of the software engineer career.
When it comes to testing. While I felt comfortable in following the TDD rules(not to write code unless writing a failed test first, only write sufficient code to pass the test), I was struggling to comprehend the concept of test doubles, and subs, mocks following up. And this came hand in hand with reading the Rspec error messages, as I couldn’t come up with a possible solution to fix my stub clause without fully understanding the error message linked with it. At one time, I wrote something like:
allow(weather).to receive(:stormy?).and_return true
However, I didn’t define a double (weather) first using weather = double :weather, or let(:weather){double :weather). Not surprising, the test kept failing!
Debugging is another thing I need to work on more. While it’s seemingly an easy process to follow, you just can’t simply migrate theory into practice. I feel I need to have a dedicated note to log all the error messages I got, and try to learn from that.
In the end, one lesson for the first week :
Everything will have to come down to habits developing. As a coder with less than 100-hour experience, I feel I need to hard-code so many concepts into my code writing. Habits can be as big as applying the debugging procedure properly (God know how struggled i was during the pre-course when my code didn’t work, without any idea of what those error messages meant!). However, habits can be as small as following a best-practice naming convention, such as: Don’t use Magic Numbers unexplained and use constant with a good name implication of the magic number instead.
Ok, see you next week and happy coding!
==========Highlights of the Week=============
Object-oriented Programming
OOP is method of computer programming that involves using ‘objects’. An object is essentially a piece of code that lets you create many similar pieces of code without actually re-writing the code each time. This is done by first writing a ‘class’, which is essentially a blueprint of the object, then you can easily create an ‘instance’ of the object, which is basically just one particular version of the object, built from the blueprint.
A class can have both methods (or ‘functions’ — basically, things that every object of that class can do) and properties (see below for examples of properties).
There are 4 major principles that make a language Object Oriented. These are :
- Abstraction: The process of picking out (abstracting) common features of objects and procedures.
- Encapsulation: The process of combining elements to create a new entity. A procedure is a type of encapsulation because it combines a series of computer instructions.
- Inheritance: a feature that represents the “is a” relationship between different classes.
- Polymorphism: A programming language’s ability to process objects differently depending on their data type or class.
Encapsulation
Encapsulation is achieved when each object keeps its state private, inside a class. Other objects don’t have direct access to this state. Instead, they can only call a list of public functions — called methods.
There are a lot of benefits using encapsulation, one of them being data hiding: The user will have no idea about the inner implementation of the class. It will not be visible to the user that how the class is storing values in the variables. He only knows that we are passing the values to a setter method and variables are getting initialised with that value.
Abstraction
Applying abstraction means that each object should only expose a high-level mechanism for using it. This mechanism should hide internal implementation details. It should only reveal operations relevant for the other objects.
The main difference between the above two is that encapsulation hides information from the caller, abstraction hides information from the callee. (Thinking from an engineer coding the software and a real user using the software).
Inheritance
Inheritance is defined as a child class can inherit all the methods from the parent class. This is an IS-A relationship.
Polymorphism
Simply put, polymorphism gives a way to use a class exactly like its parent so there’s no confusion with mixing types. But each child class keeps its own methods as they are.
One typical polymorphism in ruby is about Method Overriding defined as follow:
In object oriented programming, method overriding is a language feature in which a subclass can provide an implementation of a method which is already mentioned by its super classes.
Here’s an example:
class A
def a
puts 'In class A'
end
endclass B < A
def a
puts 'In class B'
end
endb = B.new
b.a
The method a in class B overrides the method a in class A.
Other than this 4 basic principles, there’re also some classic principles to follow. For example:
Single Responsibility Principle
This principle is based on the fact that a class should be responsible of only one aspect of a program. By responsible we mean that the class can be impacted by a change in only one aspect of the program
So basically is to make it high cohesion — — for example, to have two smaller classes that handle the two specific tasks. We have our processor that is responsible for processing and our calculator that computes the amount and creates any data associated with our new commission. Instead of mixing calculator into the processor class, so anytime to change the calculator class have to change the processor class again.
Open/Closed Principle
The Open/Closed Principle states that classes or methods should be open for extension, but closed for modification.
For example, we have the code below. But since we have hard-coded dependency on EmailSender
class. Our code collapse when the EmailSender class change, and we cannot easily extend it.
One way of doing is to use dependency injector to inject sender into that method:
class NotificationSender
def send(user, message, sender = EmailSender.new)
sender.send(user, message) if user.active?
end
endclass Sender
def send(user:, message:)
raise NotImplementedError
end
endclass EmailSender < Sender
def send(user:, message:)
# implementation for Email
end
endclass SmsSender < Sender
def send(user:, message:)
# implementation for SMS
end
end
Because NotificationSender
is accepting any sender, now we can easily extend functionality by new type of sender.
sender = NotificationSender.new
sender.send(user, "Hello World", SmsSender.new)
In this case if user is active he will receive SMS. If we want to add other sort of notification, all we need to do is to create class, inherit it from Sender
and implement send
method which would accept user
and message
.
We can extend functionality without changing implementation of NotificationSender
.
Liskov Substitution Principle
(LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping. It’s like “if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
Programming languages with strong typing have less chances to break Liskov substitution principle. Because they strictly define types of method arguments and returning values.
In case of Ruby we are responsible for that. We can easily break Liskov substitution principle by changing returning type and that would make substitution impossible:
class HomoSapiens < Human
def talk
'Hello!'
end def height
{ men: '1.70m', women: '1.62' }
end
end
Now HomoSapiens
returns hash instead of string for height
. That might break code that expects string
from height
method call.
There is also Interface Segregation Principle and Dependency Inversion Principle mentioned in the class, however, I decided to not to go into too much details this week as it’s quite a lot for me just to digest all the other principles above.
Test-Driven Programming:
So what is a test? Or what does test mean in coding?
The Four-Phase Test is a testing pattern, applicable to all programming languages and unit tests (not so much integration tests).
It takes the following general form:
test do
setup
exercise
verify
teardown
end
One big concept here is AAA.:Arrange, Act and Assert Pattern
The AAA (Arrange-Act-Assert) pattern has become almost a standard across the industry. It suggests that you should divide your test method into three sections: arrange, act and assert.
So the arrange section you only have code required to setup that specific test. Here objects would be created, mocks setup (if you are using one) and potentially expectations would be set. Then there is the Act, which should be the invocation of the method being tested. And on Assert you would simply check whether the expectations were met. For example:
// arrange
var repository = Substitute.For<IClientRepository>();
var client = new Client(repository);
// act
client.Save();
// assert
mock.Received.SomeMethod();
Unit Test vs. Feature Test
Unit tests tell a developer that the code is doing things right; functional tests tell a developer that the code is doing the right things.
Unit Test — testing an individual unit, such as a method (function) in a class, with all dependencies mocked up.
Functional Test — AKA Integration Test, testing a slice of functionality in a system. This will test many methods and may interact with dependencies like Databases or Web Services.
Three Rules of TDD
Uncle Bob describes TDD with three rules:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
Read them carefully and you’ll notice they’re simpler than they look like, and it’s obvious now that rule 3 implies rule 1, so here’s a concise version that I’ve found easier to remember:
- Write only enough of a unit test to fail.
- Write only enough production code to make the failing unit test pass.
Test Behaviour not State
Sometimes we might go into too much details in the testing, and end up repeating the code in the test subject line by line. We might try something like this:
RSpec.describe Diary do
describe "#initialize" do
it "initialises with zero entries" do
diary = Diary.new
expect(diary.entries).to eq [] #Too much detail, eg. array involved
end
enddescribe "#add_entry" do
it "adds an entry to the list" do
diary = Diary.new
diary.add_entry("hello", "world")
first_entry = diary.entries.first
expect(first_entry[:title]).to eq "hello"
expect(first_entry[:body]).to eq "world"
end
end
end
What we want to do is to test behaviour not state (the exact value being output)
Follow this process:
- Who or what is the user of this code? It might be the end user (a real person), or it might be another class
- What job is this code doing for that user?
For example:
- Question: Who, or what, is the user of this code?
Answer: The user of my Diary class is a person who is keeping their diary - Question: What job is this code doing for that user?
Answer: They add an entry so they can read it later
That second answer is the job or behaviour of the Diary class.
# So now we know that, we start testing it:RSpec.describe Diary do
describe "#add_entry" do
it "adds an entry that can be read later" do
diary = Diary.new
diary.add_entry("hello", "world")
expect(diary.print_entries).to eq "Title: hello, Body: world"
end
end
end
Test Doubles
A test double is a simplified version of an object that allows us to define “fake” methods and their return values. There are couple of different variations:
Doubles
Instead of testing ObjectA
against an instance of ObjectB
, I use a stand in (a stuntman if you like) for ObjectB
instead. ObjectA
doesn't know the difference, it simply treats the double as if were an instance of ObjectB
, but it's not - it's a dummy that I've set up with static (and therefore not variable) values.
Stubs
I want to test some behaviour of ObjectA
, but during the execution of that behaviour, ObjectA
calls a method on ObjectB
. I don't need to test that the method on ObjectB
gets called, but I do want to make sure that when it does, the method on ObjectB
always returns a specific value.
Mocks
I want to test some behaviour of ObjectA
and, critically, during the execution of that behaviour, ObjectA
must call a method on ObjectB
with specific arguments. In my test, I don't particularly care what happens afterwards, but I want to test that in the code that is about to be executed the specific method is called with the correct arguments.
Spies
I want to test some behaviour of ObjectA
and, critially, during the execution of that behaviour, ObjectA
must call a method on ObjectB
with specific arguments (sound familiar?). In my test, I don't particularly care what else happened during the test, just that in the code that was just executed the specific method was called with the correct arguments.
Debugging
Debugging procedure:
- Tighten the loop (find the exact line the bug is coming from, and the file)
- Get visibility (e.g. use p to understand what is happening, read the stack trace)
- Get a hypnosis
- Implement the hypnosis
- Run the test with error message in isolation, if it’s not working, roll back