Estratto del documento

Introduction

Techniques for implementing code that is modular, clean, and tested. Automation of the development-test-release workflow. Main programming language used is Java 8. Main focus: cleanness and maintainability of the code, correctness of the code. Most of the course will focus on automated tests:

  • Unit tests (single component)
  • Integration tests (several components together)
  • End-to-end tests (the whole application)

Also UI tests whether the application has an interface. Aim: write a script that makes sure that first the unit tests succeed, then the integration, then e2e. When a change is been made, we run a unit test on the corresponding section. Then we commit the changes to a server that runs the test script and signals eventual problems.

Course details

Focus: test-driven development. Build automation: automatically compile the code, run the tests, generate reports on test results and code quality (main tool Maven). Version control: keep track of the history of the code (main tool Git). Continuous integration: dedicated server for build automation. This means that each time a new commit has been done, the whole compile test result runs automatically and marks the commit as good/bad. For version control, Github. For continuous integration, Travis CI. This workflow also applies to team development. Virtualization: reproduction of servers on docker containers, complete reproduction of the environment.

Exam

  • A project implemented using the techniques and tools shown.
  • Oral exam on all the topics presented.

Tests

Manual tests = more time, less reliable compared to automated tests. Test = repeatable process, verifies the correct behavior of the entity under test (in a specific scenario with a given input expecting a specific output, not strictly as a returned value). The entity depends on the type of the test itself. Tests are an executable documentation, while standard documentation can get old and obsolete with changes. Tests simply fail and you have to update them. Tests provide a way not to be scared of touching the code, since they provide feedback on changes.

That being said, tests do not provide 100% proof that the code doesn’t have bugs. It makes it easier to spot and fix them. For example, you can write a failing test to reproduce the bug, then fix the code so the test succeeds, then check whether now all tests work. Regarding test types, keep in mind the “test pyramid”, with unit tests at the base and e2e tests at the top:

  • Unit tests should test a unit independently from any other components. All the other components that collaborate with the tested unit are replaced by “fake ones”. They should be many and very fast, usually run after every modification or refactor. A unit test should at least test all possible logic paths of a method (exceptions, if-then-else, etc.).
  • Integration tests happen assuming that all the units have passed unit tests and should test whether units work together as expected (at least two components). They are slower than unit tests and should be way less.
  • End-to-end tests verify that all the components work together correctly. Integration and e2e tests are often run in a dedicated continuous integration server.

SUT = System Under Test, it depends on the test:

  • In a unit test: a single Java class
  • In an integration test: either a single SUT that interacts with other components or several SUTs (instances of several classes)
  • In an e2e test: the whole application with the UI

As you approach the top of the tests pyramid, the difficulty to write tests increases. Unit tests should be white-box tests, since they are based on the knowledge of the logic of the component’s code. Meanwhile, e2e tests should be black-box, and should avoid internal details and just focus on the outcome. Integration tests are in between.

Unit tests are expected to fail the first time they run, since we write them before the implementation of the tested functionality (TDD). Meanwhile, integration and e2e are expected to succeed if run on tested components. TDD forces you to write modular, clean, and maintainable code.

NB: e2e tests are UI tests, but the opposite is not necessarily true. UI tests are hard to write (event-driven GUI), AssertJ Swift simulates user events. A unit test should be exhaustive. Meanwhile, integration shouldn’t. They should focus on the interesting cases since in unit tests it’s easy to reproduce boundary situations and exceptional contexts, while in integration tests you should just test whether the components work together.

If you make a change in the code, at least one unit test should fail. If it doesn’t, it means that tests weren’t correctly testing the code. Mutation testing frameworks verify this. It is crucial to make clear what a test verifies. Clean tests are more important than clean code.

Unit tests

We will use JUnit for unit tests. The process consists of 4 steps: setup, exercise, verify, teardown.

  • Setup stage: create the environment for the test (SUT creation, in Java create an instance of the class you want to test) test fixture. The state for the test includes the SUT and every other object needed for the test. When setup correctly, the tests are repeatable. The collaborators and/or other objects that make the SUT work will be mocked in the unit tests. In integration tests they will be real.
  • Exercise: this is the part where the code does something, accepts parameters as inputs, returns values or changes something as side effects.
  • Verify: verify that the outcome actually matches the expectations.
  • Teardown: cleanup the environment and bring it back to the initial conditions.

JUnit: Java framework for unit tests. Here a test case is another Java class that tests the Java class that needs to be tested. We verify the results with assert methods. Setup:

  • Class methods @BeforeClass
  • Static methods @Before

Corresponding methods exist for teardown: @AfterClass and @After. Lifecycle of JUnit: @BeforeClass @Before @Test @After @AfterClass and from @Before to @After are repeating for each test. Methods for assertions during the verify step:

If assert methods fail, the test terminates with failure, if an exception is thrown the test terminates with an error, otherwise it succeeds. All or nothing: if a single assert method fails, then the whole test fails. Every single test method must be executable in isolation. Don’t assume anything about the order of the tests execution. Don’t rely on side-effects. We use the JUnit framework in combo with AssertJ (assertions), Mockito (mocks), AssertJ Swift (test the UI). Don’t put the source folder of the tests in the same source folder of the code! Create a new source folder!

New JUnit 4 test, set the test folder. If we want to test a class named Foo, the test class should be named FooTest. The tests can be run with the context menu of the class. You can also run a single test method, or a test class, or a test package.

Way to call test methods: test + “name_of_method” + “condition_tested”. Example: deposit method, testDepositWhenAmountIsPositive. NB: AssertEquals(expected_value, actual_value). When AssertEquals compares 2 doubles, you have to specify a third parameter, which is the delta. When testing exception paths, we need to add try-catch construct to the test that catches exactly that exception and compare it with the message in the code. Never use another method of the class inside a test for a specific method of the same class: don’t use deposit inside a withdraw method.

Solution: add package private setters for example in order to avoid using different methods in a test for the example above, setter for the balance instead of using the deposit method. In general, don’t call methods that have logic inside them within a test method other than the tested method.

TDD

Test Driven Development has its basis on the Red-Green-Refactor cycle. This means that each feature and functionality has to be implemented in the following 3 steps:

  • Red: first you write a failing test for a specific feature. In this phase, code shouldn’t be working at all.
  • Green: then you write just the code needed to pass the test written immediately up. This code might not be pretty, clean, or perfect.
  • Refactor: in this phase, you clean up the code you have just written, making sure that each test still passes.

The important thing is that you write tests before code in order to focus on the requirements for that specific feature. If the test is written before, you make sure that the code is built with the test in mind, and that ensures that the functionality is implemented correctly and at the same time that the code gets tested.

3 laws of TDD:

  • You are not allowed to write production code unless it’s to make a failing unit test pass.
  • You are not allowed to write any more of a unit test than is sufficient to fail; compilation failures are failures.
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

If something breaks on the next cycle, going back should be easy. Small steps make it likely things can be working. Following TDD ensures that debug is done seldom. The code you write to make the test pass should be increasingly generic. Refactor: modify the code but the behavior doesn’t change. Transformation: modify the code and the behavior.

Transformation priority premise: each transformation has a preferred priority; the order is important, also on the tests! Transformations:

  • From “no code” to “code that returns null”
  • From “code that returns null” to “code that returns a constant”
  • From “code that returns a constant” to “code that returns a variable”
  • From “single statement” to “multiple statements”
  • From “no conditions” to “conditions”
  • From “conditions” to “loops”
  • From “expression” to “function”

Always try to make a test pass by implementing simple transformations first, then only if required apply complex transformations. TDD does not tell you to always proceed with small steps, but tells you that you CAN proceed with small steps when you cannot do anything else.

In order to test abstract classes, we refactor a class by extracting a superclass, ideally making it abstract and by declaring some methods as abstract. The concrete class from which we extracted the superclass has already the methods implemented. In the end, the abstract class and methods are already tested by the tests of the concrete class. Remember that if a test succeeds while it’s supposed to be failing, we have a really bad situation. Either we have a bad understanding of our code or we wrote the test in a wrong way.

Code coverage

Code coverage is a metric (percentage), and it measures how many lines of code are executed. If we refer to the test suite, code coverage tells you how many parts of the code that’s being tested while tests are running. A high test coverage (that means, a high percentage of your code that is tested by your tests) implies a low probability of bugs. Code coverage tool, by hand, would be quite hard.

JaCoCo (Java tool for code coverage) & EclEmma (front-end for JaCoCo in Eclipse). To use it, just select “Coverage As” instead of “Run As”. NB: code coverage on the tests might not be 100% even if the code coverage on the code is 100%. This can occur when you have tests for exceptions in your code. Those lines are run by the tests, but they never terminate/return any value since they throw an exception when the line is executed. NB: don’t test getters and setters. They don’t count into the real code coverage percentage. What needs code coverage is the code that contains logic.

Important: when you have to test something that needs to find an element in a collection, you have to test the method that finds the element in a collection that contains another element before the needed one. Note that 100% code coverage does not imply bug-free code or the effectiveness of the tests. But nonetheless, 100% code coverage is a necessary condition for those two aspects.

Mutation Testing

Mutation testing: technique to evaluate the quality of the tests. The idea is to mutate the SUT, that should provide a different result in the tests. Each mutation of the SUT is called a mutant. If at least one test fails using the mutant, we’re okay. Otherwise, we’re in trouble. PIT (mutation testing framework for Java) & Pitclipse (plugin for Eclipse).

Mutation testing aims to create slight variations of the SUTs that we need to test. Those muted SUTs are called mutants, and they’re used to verify if the tests are written correctly. The aim of this test is to, after we write correct tests, make them fail by altering the SUT itself. If the mutant “survives” after running the tests again, then the tests are insufficient.

Anteprima
Vedrai una selezione di 6 pagine su 24
Advanced programming techniques Pag. 1 Advanced programming techniques Pag. 2
Anteprima di 6 pagg. su 24.
Scarica il documento per vederlo tutto.
Advanced programming techniques Pag. 6
Anteprima di 6 pagg. su 24.
Scarica il documento per vederlo tutto.
Advanced programming techniques Pag. 11
Anteprima di 6 pagg. su 24.
Scarica il documento per vederlo tutto.
Advanced programming techniques Pag. 16
Anteprima di 6 pagg. su 24.
Scarica il documento per vederlo tutto.
Advanced programming techniques Pag. 21
1 su 24
D/illustrazione/soddisfatti o rimborsati
Acquista con carta o PayPal
Scarica i documenti tutte le volte che vuoi
Dettagli
SSD
Scienze matematiche e informatiche INF/01 Informatica

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher ElenaSmith di informazioni apprese con la frequenza delle lezioni di Advanced programming techniques e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università Università degli Studi di Firenze o del prof Lorenzo Bettini.
Appunti correlati Invia appunti e guadagna

Domande e risposte

Hai bisogno di aiuto?
Chiedi alla community