Professional Documents
Culture Documents
An analysis of how differences between Clojure and Java affects unit testing and
design patterns
(En analys av hur skillnader mellan Clojure och Java påverkar enhetstestning och
designmönster)
NICLAS NILSSON
1 Introduction 1
1.1 Enter Clojure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Problem statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3.1 Scope and limitations . . . . . . . . . . . . . . . . . . . . . . 3
2 Background 5
2.1 The agile industry . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1 Classical product development . . . . . . . . . . . . . . . . . 5
2.1.2 Responding to change . . . . . . . . . . . . . . . . . . . . . . 5
2.1.3 Going agile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1.4 Demands of the agile process . . . . . . . . . . . . . . . . . . 6
2.1.5 Sibling methods and principles . . . . . . . . . . . . . . . . . 7
2.2 Software testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.1 Classes of tests . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.2 Test automation . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.3 Test-Driven Development . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.1 Core idea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.2 Benefits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.3 Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3.4 Test-First vs. Test-Last . . . . . . . . . . . . . . . . . . . . . 10
2.4 Continuous Integration . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4.1 Pre-conditions . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4.2 Automation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4.3 Continuous Deployment . . . . . . . . . . . . . . . . . . . . . 12
2.4.4 Symbiosis with Test-Driven Development (TDD) . . . . . . . 12
2.5 Design patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.1 Dependency injection . . . . . . . . . . . . . . . . . . . . . . 13
2.5.2 Dependency mocking . . . . . . . . . . . . . . . . . . . . . . . 14
2.5.3 Tests as documentation . . . . . . . . . . . . . . . . . . . . . 15
2.6 Clojure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.2 Data types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.6.3 Concurrency . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6.4 Resource referencing . . . . . . . . . . . . . . . . . . . . . . . 18
2.6.5 Immutable values . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.6.6 Platform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.6.7 Special forms . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6.8 Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3 Method 21
3.1 Litterature study . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Programming activity . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4 Results 23
4.1 How Clojure differs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 Testing macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.3 Dependency injection in Clojure . . . . . . . . . . . . . . . . . . . . 25
4.3.1 Each dependency as an argument . . . . . . . . . . . . . . . . 26
4.3.2 Set of dependencies as a map . . . . . . . . . . . . . . . . . . 26
4.3.3 Set of dependencies as a protocol . . . . . . . . . . . . . . . . 27
4.3.4 Dependencies as context . . . . . . . . . . . . . . . . . . . . . 28
4.4 Untestable code in namespaces . . . . . . . . . . . . . . . . . . . . . 28
4.5 Mocking in Clojure . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.5.1 Mocking injected functions . . . . . . . . . . . . . . . . . . . 29
4.5.2 Mocking non-injected functions . . . . . . . . . . . . . . . . . 30
4.5.3 Verifying function calls . . . . . . . . . . . . . . . . . . . . . . 30
4.6 Documenting Clojure . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.6.1 Expressiveness of tests in Clojure . . . . . . . . . . . . . . . . 32
5 Discussion 35
5.1 Lack of IDEs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
5.2 Problems of debugging . . . . . . . . . . . . . . . . . . . . . . . . . . 36
5.3 Dynamic typing and productivity . . . . . . . . . . . . . . . . . . . . 38
5.4 Mocking made easier . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
5.5 The REPL and automated tests . . . . . . . . . . . . . . . . . . . . . 39
6 Conclusions 41
Bibliography 43
Chapter 1
Introduction
The computer industry today is an exciting industry to be a part of. The develop-
ment of new technology is happening at record speeds with new devices, interfaces
and ways of interacting with the world quickly becoming essential parts of our every
day lives at a rate that can only be described as revolutionary.
Along with this increase in the amount of computer products present on the
market comes the increased need for talented developers who can quickly create
stable and maintainable software that satiates the markets needs for quality prod-
ucts. This pressure of shortening the time between a product’s conception to its
release becomes problematic when practicing the old waterfall-style of development
that tends to favor large scale, monolithic software within large timeframes and
budgets. Couple this with the improvements in technology that allows development
teams to release their products with the click of a button and it’s no wonder that
agile methods have become so prevalent.
As more and more teams adopt agile methods in their every day work the pitfalls
of the iterative development process become ever more apparent. When products
continually grows and changes so does also the need for testing. It’s not longer
practically possible to postpone the product’s testing phase towards the end of its
development. As the rate of development changes, so must also the rate of its
testing. For the development process to be stable over time without a dramatic
increase in technical debt the testing of a product must be as continuous as its
increase in functionality. The longevity of the development process has at times
been described as:
A development method that began to emerge around the same time as agile meth-
ods were formulated was the method of TDD. This method embraces the testing
part of software development by not viewing tests as simply a means of verifying
program behavior but instead considering them to be the key driving force behind
the software’s development and design. TDD depends on the presumption that
tests are quick to execute and the development community has as a response to
1
CHAPTER 1. INTRODUCTION
this found ways of reducing the time needed to run tests through the use of de-
sign patterns. As Object-Oriented (OO) programming languages have been the
most prominent languages in the industry during the last decade [6] these design
patterns have mainly been created to fit into the OO paradigm.
The deceleration in recent years of the number of transistors that can be fit into a
single processor together with the ever increasing need for processing large data sets
has sparked the movement from single core to multicore processing. Harnessing the
power of these new generation multicore processors requires that developers pro-
duce multithreaded applications, which opens up the possibility for several kinds
of architectural problems of shared memory, thread locking etc. These problems
must often be addressed manually when developing software in popular OO pro-
gramming languages such as Java and the manual thread management can quickly
become complex.
As a response to this problem of manual thread management came the pro-
gramming language Clojure, which aims to eliminate most problems associated
with multithreading while at the same time providing a light weight alternative to
large enterprise languages such as Java.
Clojure is however a fundamentally different language from these popular OO
languages. This means that design and testing strategies that have been devel-
oped in these languages aren’t always directly applicable when developing software
in Clojure. Clojure also brings new concepts to the table that OO programmers
probably aren’t used to and thus have no experience of testing.
1.2 Motivation
2
1.3. PROBLEM STATEMENT
• How do these differences affect the creation and execution of unit tests?
• How do these differences affect the test first TDD process when developing in
Clojure?
3
Chapter 2
Background
5
CHAPTER 2. BACKGROUND
may be redirected. Response to these changes is hindered by the linear style of the
waterfall model, which has encouraged the search for a different method of product
development that is able to deal with critical changes. An answer came 2001 in the
form of agile software development [9].
6
2.2. SOFTWARE TESTING
the code produced by one team member must be comprehensible to other team
members.
Teams are practically required to be small in size and self organizing when there
isn’t a large overall requirements document. As personal communication becomes
more key to success this also limits the team’s size for practical reasons. Require-
ments are worked on one or a few at a time and only the team is assumed to know
best how they should be implemented. The relative independence of the team also
implies that it has to be cross functional where all of the major competences needed
to develop the product are present within the team.
The product has to be in a deliverable state after each development iteration.
Since each sprint typically ends with a demonstration of the product this of course
implies that it can’t be broken at the time of demonstrating and that all of its
functionality must be fully functional; otherwise there would be no way for the
product owner to assure that the requirements have been met.
Testing must be performed often when iterations are short and the product is
assumed to be in a continuously deliverable state. Both newly implemented as well
as old functionality must be frequently tested as the modification of existing code
can easily break what wasn’t broken in the past.
7
CHAPTER 2. BACKGROUND
• Unit tests operate on a low level and are tightly coupled with source code.
In the best of worlds one unit test always correlates to one minimal piece of
functionality without dependencies to other components, and when this is the
case they are quick to execute.
• Integration tests validate that software components work correctly when com-
bined with each other. Only the interfaces between components are tested
and it’s assumed that each component has been individually unit tested.
• System tests spans the entire system where most or all components have to
be set up and interconnected. This may increase the execution time of the
tests since it demands more computing power and introduces latency between
components - especially when connecting to external services.
• Usability tests are used to examine how users interact with the product and
how it impacts their overall experience of using the product.
• Acceptance tests determine that the product reaches quality standards set up
by the product’s stakeholders.
8
2.3. TEST-DRIVEN DEVELOPMENT
2. Implement the new functionality and run all tests, both the new and pre-
existing ones. Repeat until all tests pass.
3. Clean up the code and ensure that all tests still pass, then return to step 1.
The focus on implementing minimal pieces of functionality one at a time means that
the time it takes to complete each iteration generally ranges between 30 seconds
to a few minutes. This process becomes cumbersome when tests take a long time
to execute and it’s therefore recommended for developers to focus on writing unit
tests instead of system tests.
2.3.2 Benefits
Kent Beck argues in his book Extreme Programming Explained for the following
benefits of practicing TDD [4]:
• Quick feedback. The programmer is instantly aware of whether new function-
alities work as intended and if any already existing functionality has been
broken.
• Task orientation. Writing small tests for each piece of functionality forces the
programmer to decompose problems into small, manageable tasks.
• Focus. Having a single failing test to fix supports the programmer in focusing
on a individual tasks.
• Progress. Unit tests become a reasonable metric for measuring the flow and
progress of the development process.
9
CHAPTER 2. BACKGROUND
• Maintainability. Further work on the software is made easier when all imple-
mented functionality can be tested.
• Testable software. It’s indeed possible to write untestable code. This kind of
code can be a good place for bugs to remain undetected.
2.3.3 Effects
Several extensive studies have been made with the intent of measuring the effec-
tiveness of TDD, but the observed effects seem to vary between individual projects
where the observed effects encompass productivity, defect density, maintainability
and code coupling.
Most studies observe positive effects on maintainability, defect density and code
coupling but with varying productivity. Some observe a reduction in productivity
[34], some observe an increase in productivity [16] while some finds no observable
effects [15].
10
2.4. CONTINUOUS INTEGRATION
2.4.1 Pre-conditions
The frequent merging of different branches practically necessitates the existence
of unit and integration tests. While it’s certainly possible to practice CI without
automated testing this will quickly become an uncertain and tiresome process, as
developers have little or no evidence of whether merges are successful or not if they
don’t manually perform tests after each merge (which takes time and effort).
Manually performing merges several times a day quickly becomes unmanageable
and several developers often have the need to create local branches of the same
source files. There is thus a need for teams to use a central source control system
and tools assisting them in the merging of code. Luckily several of these systems
exist - the most widely adopted today arguably being git [1] and Subversion [2],
both of which allow creating local branches and automatic branch merging.
2.4.2 Automation
A shared, automated build server can be used by teams practicing CI for a small
initial investment in effort with a high payoff in long term automation. The build
11
CHAPTER 2. BACKGROUND
server is responsible for automatically running unit and integration tests, but more
general quality assurance procedures such as measuring and charting performance
over time or extracting and compiling documentation can also be performed auto-
matically. The general idea here is that the effort spent constructing automated
processes will in the end be far outweighed by the benetits that CI brings with it.
12
2.5. DESIGN PATTERNS
• Interface injection: basically the same as setter injection, but the setter meth-
ods are declared in interfaces implemented by client objects.
public Logger() {
this.database = new DatabaseExample();
}
13
CHAPTER 2. BACKGROUND
Now assume that we want to create several instances of the logger class where each
instance writes to a different kind of database. This simply isn’t possible in the
above example as each instantiation of the Logger class always creates a connection
to the same database class. Consider the following class that resolves this issue
through constructor injection:
public class Logger {
private DatabaseExample database;
14
2.5. DESIGN PATTERNS
In the late 90’s a group of developers and system architects began researching ways
of eliminating these kinds of problems in OO systems [20] and the result was a test
design pattern called object mocking [19].
A mocked object is an instance of an object that behaves as an instance of a
certain class but with functionality that is tailored for special use cases without
relying on an actual implementation of the class. They are mainly used in unit
testing of clients, where the values and methods of the services a client is dependent
on are mocked and injected into the client without introducing any actual coupling
between existing components.
Although there has been some controversy regarding the terminology of mocked
objects in term of the functionality they provide [23] there will be no such distinc-
tions made in this thesis and all all sorts of mocked objects will simply be referred
to as mocked objects.
Unit testing components with dependencies is practically impossible without the
mocking of objects. Since TDD relies heavily on unit testing, object mocking is thus
a crucial part of the Test-Driven Development process.
15
CHAPTER 2. BACKGROUND
able to extract information from the test about what functionality is implemented
and examples of how it’s supposed to be used.
The view point of keeping unit tests as documentation has several advantages
when used in conjunction with TDD. Firstly the tests are always up to date and
consistent with the functionality described as they are run continuously during
development when practicing TDD. One therefore never have to worry that the
behavior described is inconsistent with the actual behavior of the software. Similarly
there’s also the positive effect of having no undocumented behavior since TDD
strives for total test coverage of all implemented functionality. Another implicit
effect is the fact that developers won’t have to commit extra time to writing separate
documentation as the tests are already there.
This method does however require that the developers put effort into making
the tests expressive, since other people (and at a later point in time they themselves
too) are supposed to be able to quickly understand which behaviors are being tested.
A common way of achieving this goal is to make each unit test verify one certain
behavior and name it in a descriptive way; an example of how this might look in
Java using JUnit is:
@Test
public void twoPlusThreeEqualsFive() {
assertEquals(5, 2 + 3);
}
Apart from naming the tests most languages support some way of annotating them
with actual text phrases that are supposed to be a description of the tests. By
assembling all of the tests into a list one should this way be able to get a full
description of all behaviors that can be expected from a piece of software and
examining into individual tests provides usage examples.
2.6 Clojure
Clojure is a relatively young programming language with version 1.0 released in
2009 [32]. It is in the words of its creator Rich Hickey [33] a language based around
the core ideas of having a programming language that is a functional Lisp dialect
with an emphasis on immutability and concurrency, while being designed to run
on host platforms with a great deal of transparency to the underlying platform’s
libraries and capabilities.
2.6.1 Syntax
Clojure belongs to the Lisp family of languages - a group of languages that are
all based on a computational model and syntax first described in the late 50’s by
John McCarthy [29]. As with other Lisp dialects all code and data are syntactically
and computationally interchangeable in Clojure, where expressions are written as
16
2.6. CLOJURE
paranthesized lists. The first item in a list is interpreted as a function with the
following items being arguments to the function:
Lists, the most important data type in Clojure, are created with function calls:
(list 1 2 3) => (1 2 3)
but in practice lists are often created using syntactic sugar for readibility:
’(1 2 3) => (1 2 3)
’(1,2,3) => (1 2 3)
’(1 2 3)
[1 2 3]
#{1 2 3}
Symbols beginning with a colon are interpreted as atomic keywords. The four main
collection data types share some properties in the sense that they are sequences of
values. They are thus commonly referred to as seqs when their sequence properties
are applicable and seqs can be lazy. Since Clojure is a dynamically typed language
values of different types are thus interchangable in code and their type is checked
at runtime rather than compilation. Collections are allowed to contain values of
different types.
17
CHAPTER 2. BACKGROUND
2.6.3 Concurrency
Clojure is a language with the concept of concurrency built into its core [25]. The
main creator of Clojure, Rich Hickey, has in his work with Clojure reduced problem
of concurrency into the identity-value conflict - the inability to separate references
to values from the values themselves [26]. A reference shared by multiple executing
threads may be altered to refer from one value to another by one of the threads and
this alteration will be visible by the other threads. Similarly the values that are
being referred to are stored in physical memory locations and may also be altered by
other threads during their execution, leading to classical race conditions when mul-
tiple threads reads and writes to the same memory locations simultaneously. When
dealing with concurrency one must thus address the problems of the coordination
of references and the mutability of values. Clojure attempts to solve the former
problem by supplying and forcing the use of several methods of thread-safe resource
management in which references to shared resources are handled and coordinated
in safe ways between threads that doesn’t lead to race conditions and the latter by
implementing a memory model of immutable values where values once never change
after they have been set.
18
2.6. CLOJURE
The common use cases for Atoms are when multiple threads needs access to and the
ability to modify a single resource without the occurrence of race conditions, while
Refs are similarly used for grouping together several resource modifications into a
single atomic transaction. Agents are on the other hand commonly used when the
modification of a resource is asynchronous and the thread accessing the resource
doesn’t have to wait for the modification to execute.
2.6.6 Platform
Clojure is hosted on the JVM platform and shares its garbage collection, threads
etc. Clojure source code is interpreted and compiled to JVM bytecode either ahead
of time or at runtime and Clojure supports the ability to natively implement Java
interfaces and classes. It’s also possible in Clojure to interact with existing Java
code and developers thus have easy access to already existing Java code.
There has been some attempts made to port the Clojure language to other
platforms as well, some notable examples being ClojureScript [30] that compiles
Clojure code to JavaScript and ClojureCLR [31], a Clojure implementation on the
.NET platform. These spin offs aren’t however within the scope of this thesis and
- although interesting in their own right - won’t be discussed in detail.
19
CHAPTER 2. BACKGROUND
2.6.8 Macros
A macro is a tool for altering a data structure into code which can then be in-
terpreted by the Clojure compiler. Macros can be defined and expanded by pro-
grammers, thus allowing the core language to be extended with new syntactical
constructs. This is made possible by the fact that Clojure is homoiconic, ie. the
structure of its source code is the same as the structure of its abstract syntax tree.
An example of a Clojure macro is the "thread-first" macro:
->
is evaluated as:
20
Chapter 3
Method
The methods used in this thesis consists of a study and analysis of literature, as
well as programming activities. The work began with an extensive literature study
along with an analysis of how OO practices could be applied in Clojure, specifically
taking into consideration of the differences between Java and Clojure. To get a first
hand understanding of how the TDD process differs between development in Java
versus Clojure and what consequences these differences may lead to there were two
identical programs created in both languages.
21
CHAPTER 3. METHOD
Clojure and one in Java - that have identical external interfaces and thus can be
considered to provide the same functionality.
For the programming activity to be relevant it was important that the applica-
tions touched on the more complex parts of unit testing and the criteria put on the
applications were that they should:
There were however no demands made on the performance of the applications, other
than that they were reasonably similar and effective.
The application that was chosen to be developed in both languages was an
implementation of the game Checkers [18] that has a text only interface and is run
by automated AI’s. It’s assumed that the time it took to develop two versions of
the game was within the scope of the time available for this thesis.
These implementations were chosen for several reasons. First of all the minimal
text based UI and the fact that AI’s play the game leads to little user interaction.
Testing user interaction is thus limited and more focus can be put into testing game
functionality with unit tests. Secondly there are several variants of Checkers with
different board sizes and rules, which opens up for the possibility of modularity,
inversion of control and dependency mocking. Lastly there’s no need for external
language-dependent libraries, which means that the implementations can be com-
pletely written in native code.
22
Chapter 4
Results
• Dynamic execution
• Dynamic typing
• Lisp syntax
• Macros
• Namespaces
• Immutable values
• Transactional memory
• First-class functions
• No object-orientation
• Creating documentation
By closely examining this list of features it’s possible to observe and deduce how
they may affect unit testing and the TDD process.
23
CHAPTER 4. RESULTS
Observation 1 - Macros
The arguably most striking difference between the two languages is the fact that
macros are a core feature of Clojure but not Java. There’s no practical way of
creating self modifying code in Java and as a consequence of this there’s also no
experience in how such code should be tested. Section 4.2 will explain and motivate
one way of testing macros in Clojure.
Observation 4 - Documentation
Documentation is an important part of all software projects. In Java the JavaDoc
tool is industry standard for coupling documentation and code together but Clojure
doesn’t have a similar tool. One must thus find another way of documenting code.
The principle of keeping unit tests as documentation might also be different in
Clojure when compared to Java due to the differences in syntax and test tooling.
Section 4.6 will explain ways to tackle these two problems in Clojure.
24
4.2. TESTING MACROS
To test that the macro works as intended we simply expand the macro on a sample
expression and compare it to the expected result:
It should be obvious that testing macros is just like testing regular list transfor-
mations, which shouldn’t be an unfamiliar task to reasonably experienced Java
programmers. The above above example is of course rather simple and macros may
be structured in more complex ways, but the principle of list transformation still
stays the same.
25
CHAPTER 4. RESULTS
A client function that depends on these two operations might look something like:
The service functions might with this simple design be mocked when testing or
replaced with other implementations with the same API. Calling the client function
with implementations of its dependencies is trivial:
A problem with this design is that as the number of dependencies increase for the
client function it may become an uncumbersome way to redirect dependencies. Code
becomes more cluttered and thus less readable, as well as making refactoring more
troublesome.
26
4.3. DEPENDENCY INJECTION IN CLOJURE
And the client function would be called with the map as an argument:
(defprotocol Storage
(save [this coll id])
(fetch [this coll id timeout]))
And the protocol may have multiple service implementations that are created with
the reify macro:
(def db-storage
(reify Storage
(save [this coll id]
( ... implementation of function ...))
(fetch [this coll id timeout]
( ... implementation of function ...))))
(def in-memory-storage
(reify Storage
(save [this coll id]
( ... implementation of function ...))
(fetch [this coll id timeout]
( ... implementation of function ...))))
These service implementations may later be passed to the client function regardless
of which implementation is used:
27
CHAPTER 4. RESULTS
The service functions must then be bound in the context which calls the function:
Though it’s entirely possible to use this approach effectively it has two important
implications. Firstly the compiler will complain upon loading the client function
that the service functions aren’t defined within the client function’s context. The
service functions must thus be bound to some default value for the code to be able
to compile. Secondly when dependencies are purely contextual two calls to the
same function and with the exact same arguments may produce differing results
depending on where they are placed in the code, as opposed to calls to functions
whose dependencies are all arguments.
The fact that the function creates different results depending on the context in
which it’s executed means that it’s impure, while the previous examples of client
functions that aren’t context dependent are pure (assuming that the service func-
tions are pure as well). Pure functions are generally easier for programmers to wrap
their heads around and reason about since one doesn’t have to expand one’s focus
outside of the internal context of a function. It’s therefore recommended to opt
for the approach of passing dependencies as function arguments rather than having
dependencies as bound by context.
28
4.5. MOCKING IN CLOJURE
In Clojure there are no classes and therefore no constructors either, but a similar
problem still exist with namespaces. By design Clojure evaluates all expressions
present within a namespace as it is loaded at runtime. This means that any loose
logic within a namespace that is something other than a pure definition will be run
as the namespace is loaded. As an example of this consider the following namespace:
(ns example-namespace)
(print "Hello")
Loading this namespace will apart from bringing the var and function into scope
also print the string "Hello", i.e. the loading of the namespace is not free from side
effects. Although this example has a rather harmless side effect it’s possible that
there may be code run that leads to more complex behaviors. These behaviors are
in the best of cases hard to test and in the worst untestable, where dependencies are
impossible to mock. For the sake of testability one should thus avoid including any
logic in namespaces that will be evaluated when the namespace is loaded if possible.
If one wants to test this client function without depending on an actual implemen-
tation of the service function add-fn one will have to set up a mock function that
can be used as an argument in a unit test:
(deftest test-add-and-square
29
CHAPTER 4. RESULTS
In this example the mocked function add-fn always returns 5, regardless of which
arguments it’s called with. Instead consider that one wants to make a mocked
function that returns different values depending on which arguments it’s called
with. One arguably simple way of doing this is by pattern matching and mapping
the expected input arguments to custom output values:
(deftest test-add-and-square
(let [add-fn (fn [x y]
(case [x y]
[2 3] 5
[3 5] 8))]
(is (= 25 (add-and-square add-fn 2 3)))
(is (= 64 (add-and-square add-fn 3 5)))))
(defn add-and-square [x y]
(let [z (add-fn x y)]
(* z z)))
If we want to create a true unit test for this client function we must be able to do
this without depending on an actual implementation of add-fn. This is possible by
rebinding the symbol bound to add-fn in the context in which add-and-square is
executed:
(deftest test-add-and-square
(with-redefs [add-fn (fn [x y] 5)]
(is (= 25 (add-and-square 2 3)))))
30
4.6. DOCUMENTING CLOJURE
Verifying that a function has been called a number of times in Clojure - with the
ability to check with which arguments the function was called - is a bit trickier than
just mocking function behavior without the use of frameworks, but its’ possible to
do this in a few rows of native Clojure code. One way of doing this is:
(deftest test-parse-if-active
(let [event {:status :active, :value :some-value}
args (atom [])
parse-fn (fn [event] (swap! args conj event))]
(parse-if-active parse-fn event)
(= @args [event])))
This approach creates a mutable vector args that will contain the arguments that
the mocked service function parse-fn will be called with. parse-fn is implemented
in such a way that each time that it’s called it will append its input arguments to the
args vector. Checking if the parse-fn function was called with certain arguments
is then just a matter of inspecting the args vector. Counting the number of times
the parse-fn function was called is equal to counting the number of elements in
args and since the each insertion into the vector is done at its tail it’s possible
to check in which order multiple calls to the function was made by doing index
comparisons.
/**
* The mathematical operation of exponentiation.
*
* @param b The base of the operation.
* @param n The exponent of the operation.
* @return b to the power of n (b^n).
*/
public int exp(int b, int n) {
... implementation of method ...
}
31
CHAPTER 4. RESULTS
(defn exp
"The mathematical operation of exponentiation.
Returns the value of b to the power of n (b^n) where
b is the base and n is the exponent of the operation."
([b n]
(... implementation of function ...)))
Both methods requires that developers maintain the documentation manually and
there’s thus little difference between them in practice.
• follow the body of a test and understand what is being tested, and
As will be shown it’s in fact true that Clojure’s native testing environment satisfies
all of these conditions.
In order to test an individual piece of functionality within a single test one must
to be able to separate the functionality under test from its dependencies. As we have
already shown it’s entirely possible to mock dependencies in Clojure and assuming
that the functionality under test has been implemented in a modular fashion it’s
therefore possible to create unit tests that test an individual piece of functionality.
It shouldn’t be a problem for a developer to look at the body of a test and
understand what is being tested as long as the test is cleanly written and the
developer has a decent understanding of the language.
Giving a meaningful description of a test can in practice be done in two ways
- either by linking the test with a descriptive text string or by giving the test a
name that is descriptive of the functionality that is being tested. Since it’s possible
to name functions in clojure using regular letters and many of the other ASCII-
characters the naming of functions can be done in an arguably descriptive way:
(deftest two-plus-three-is-five
(is (= 5 (+ 2 3))))
32
4.6. DOCUMENTING CLOJURE
If one for some reason would rather describe a test in the form of free text this is
supported by Clojure’s native testing framework by associating the assertion with
a string:
(deftest a-test
(is (= 5 (+ 2 3))) "two plus three should be equal to five")
(deftest several-tests
(testing "mathematical operations"
(testing "addition"
(is (= 5 (+ 2 3)))
(is (= 8 (+ 6 2))))
(testing "multiplication"
(is (= 8 (* 4 2)))
(is (= 0 (* 0 7))))))
Regardless of which way one chooses to describe one’s tests it should be obvious
that it’s indeed possible to create just as expressive and descriptive tests in Clojure
as one may in Java.
33
Chapter 5
Discussion
This thesis is based around the hypothesis that the differences between Clojure and
Java would in turn lead to great differences in how unit testing is done and TDD is
practiced in both languages respectively. As this hypothesis was analyzed closer it
became more and more apparent that the results expected to be found weren’t as
dramatic as first assumed.
This is likely due to the fact that even though the languages differ much in
appearance and implementation they still operate within the same domain. Being
general purpose languages means that everything that is possible to do in Clojure
is also possible in Java. Although the manner in which developers may choose
to create their implementations and thus their unit tests may differ, the fact still
remains that unit tests are simply small, modular tests that test certain pieces of
functionality. As long as it’s possible to separate dependencies it’s also possible to
create idiomatic unit tests. Thus the concern of trying to find the differences in how
unit testing is affected by the lingual differences is mainly the problem of trying to
create modular code without couplings.
One greatly impactful difference between the two languages unrelated to depen-
dency separation is the tools available to developers that may be used for develop-
ment in each language. Both languages have active communities that continuously
find new problems and invent tools that attempts to solve them and one can’t for-
get that each individual developer has the freedom to choose her own development
environment. This makes it difficult to make any sort of analysis on tooling differ-
ences and any attempt to do so would merely be a snapshot of the current situation
that would quickly become outdated. It’s however safe to say that the fact that
Clojure has been around for a significantly shorter time than Java means that there
are naturally fewer and less mature tools for Clojure developers to choose from.
Perhaps this will change in the future, though.
35
CHAPTER 5. DISCUSSION
36
5.2. PROBLEMS OF DEBUGGING
macro. This does however require that breakpoints are inserted directly into the
source code rather than as flags in the editor. It also requires that the program
is executed interactively through the REPL, which isn’t suitable for all circum-
stances. Although a usable method in small projects it’s doubtful that it’s practical
to implement in larger projects.
The arguably most complete external tool suite for debugging Clojure code is
Ritz [41], an Emacs plugin that supports management and inspection of a programs
state during execution. Using an Emacs plugin isn’t however a suitable choice for
many developers and the user friendliness of Ritz’s text-based interface is debatable.
As a program crashes during execution it’s useful to be able to extract infor-
mation about what caused the crash. Clojure, like many other languages, provide
this information through the presentation of a stack trace along with a simple error
message. Consider the following function that (wrongly) tries to apply a number as
a function:
(defn -main []
(dorun (map 5 ["a" "b" "c"])))
This will cause an error during execution and the developer is provided with a
stacktrace:
37
CHAPTER 5. DISCUSSION
at clojure.lang.Var.applyTo(Var.java:532)
at clojure.main.main(main.java:37)
As can be clearly seen this relatively simple error provides a long, cryptic stack
trace that reveals much of the internal workings of Clojure and which doesn’t help
the developer much in tracing the source of the crash, especially when logic is nested
through several layers of function calls.
Overall one could argue that debugging Clojure code is much harder than de-
bugging Java code, mainly due to the fact that there exists little IDE support for
inspecting executing programs and error tracing. This is sure to affect the develop-
ment process negatively, regardless of whether TDD is practiced or not.
38
5.5. THE REPL AND AUTOMATED TESTS
39
Chapter 6
Conclusions
Although Java and Clojure are two fundamentally different languages this does not
have a big impact on how unit tests are created. It was observed that the basic
component with regards to code modularization is the class in Java and the function
in Clojure. This leads to different implementations of the dependency injection
pattern where dependencies are injected directly into function calls in Clojure. The
dynamic typing of Clojure allows for mocked dependencies to be easily created
inlined in code without the need for external frameworks.
Logic placed in constructors of Java classes is difficult to test at best and
untestable at worst. Clojure faces a similar problem stemming from a design fea-
ture that allows code within a namespace to be executed when the namespace is
loaded. Such code should be avoided whenever possible. Similar parallels can be
drawn between the testing of macros, where testing a macro is the same as testing
a list transformation - a task that should feel comfortable for any experienced Java
developer.
There is currently a noticable deficit in the development tooling ecosystem of
Clojure as compared to Java. The interactive REPL may possibly lead to increased
productivity but there is a lack of extensive debugging tools and user friendly edi-
tors which might negatively impact the TDD process by slowing down the flow of
development. Clojure has however not been around for long as an established pro-
gramming language, so it’s possible that this may come to change in the foreseeable
future.
41
Bibliography
43
BIBLIOGRAPHY
44
BIBLIOGRAPHY
45
www.kth.se