Cómo implementar la interfaz de comparación de un Handler en C++
¿Tienen sentido las siguientes implementaciones?
static int compare(const Handler&, const Handler&);
int compareTo(const Handler&); // member function
Ver stackoverflow
Sobrecarga de operadores en C++:
template <typename T>
struct Handler {
T id;
explicit Handler(T v) : id(std::move(v)) {}
bool operator==(const Handler& other) const = default;
auto operator<=>(const Handler& other) const = default;
};
using NumericHandler = Handler<int>;
using StringHandler = Handler<std::string>;
int main() {
StringHandler a{"EMP-001"};
StringHandler b{"EMP-002"};
auto ord = (a <=> b); // OK
NumericHandler c{123};
// bool same = (a == c); // ERROR: tipos distintos
}

¿Por qué? ¿Cómo funciona?
Saludo.java sin bibliotecas de pruebas unitarias?class Saludo {
/**
* Imprime "Hola Mundo!"
*/
void saludar() {
System.out.println("Hola Mundo!");
}
/**
* Imprime un mensaje
*/
void saludar(String mensaje) {
System.out.println(mensaje);
}
Incluir un método main que pruebe la funcionalidad de la clase:
/**
* Tests
*/
public static void main( String[] args ) {
Saludo saludo1 = new Saludo();
saludo1.saludar();
Saludo saludo2 = new Saludo("Hola caracola!");
saludo2.saludar();
}
}
Cuanto más grande sea la interfaz de la clase, mayor será el main
El tamaño del código de la clase crece por las pruebas
Poco fiable, porque main forma parte de la misma clase y tiene acceso a los elementos privados
Difícil de automatizar las pruebas, incluso pasando argumentos a main
import org.junit.*;
import static org.junit.Assert.*;
public class SaludoTest {
public static void main(String args[]) {
junit.textui.TestRunner.run(SaludoTest.class);
}
@Test
public void saludar() {
Saludo hola = new Saludo();
assert( hola!=null );
assertEquals("Hola Mundo!", hola.saludar() );
}
}
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
public class MyTestRunner {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(SaludoTest.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
}
}
¿De qué están hechas las anotaciones como @Test?
Veamos una versión anterior de jUnit, que expone más claramente las tripas del framework
import junit.framework.TestCase;
import junit.framework.Assert;
public class SaludoTest extends TestCase {
public SaludoTest(String nombre) {
super(nombre);
}
public void testSaludar() {
Saludo hola = new Saludo();
assert( hola!=null );
assertEquals("Hola Mundo!", hola.saludar() );
}
}
Diseño de una aplicación de comercio electrónico:
ShoppingCart - carrito de la compraCreditCard - tarjeta de créditoProduct- artículosDiseño de pruebas unitarias de ShoppingCart para:
public class ShoppingCart {
private ArrayList items;
public ShoppingCart() { ... }
public double getBalance() { ... }
public void addItem(Product p) { ... }
public void removeItem(Product p)
throws ProductNotFoundException { ... }
public int getItemCount() { ... }
public void empty() { ... }
public boolean isEmpty() { ... }
}
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.framework.Assert;
public class ShoppingCartTest extends TestCase {
private ShoppingCart bookCart;
private Product defaultBook;
//...
protected void setUp() {
bookCart = new ShoppingCart();
defaultBook = new Product("Extreme Programming", 23.95);
bookCart.addItem(defaultBook);
}
protected void tearDown() {
bookCart = null;
}
public void testEmpty() {
bookCart.empty();
assertTrue(bookCart.isEmpty());
}
public void testProductAdd() {
Product book = new Product("Refactoring", 53.95);
bookCart.addItem(book);
double expectedBalance = defaultBook.getPrice() + book.getPrice();
assertEquals(expectedBalance, bookCart.getBalance(), 0.0);
assertEquals(2, bookCart.getItemCount());
}
public void testProductRemove() throws ProductNotFoundException {
bookCart.removeItem(defaultBook);
assertEquals(0, bookCart.getItemCount());
assertEquals(0.0, bookCart.getBalance(), 0.0);
}
public void testProductNotFound() {
try {
Product book = new Product("Ender's Game", 4.95);
bookCart.removeItem(book);
fail("Should raise a ProductNotFoundException");
} catch(ProductNotFoundException success) {
...
}
}
public static Test suite() {
// Use reflection to add all testXXX() methods
TestSuite suite = new TestSuite(ShoppingCartTest.class);
// Alternatively, but prone to error when adding more
// test case methods...
// TestSuite suite = new TestSuite();
// suite.addTest(new ShoppingCartTest("testProductAdd"));
// suite.addTest(new ShoppingCartTest("testEmpty"));
// suite.addTest(new ShoppingCartTest("testProductRemove"));
// suite.addTest(new ShoppingCartTestCase("testProductNotFound"));
return suite;
}
}
Podemos agrupar varios casos de prueba en una misma suite:
import junit.framework.Test;
import junit.framework.TestSuite;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
public class EcommerceTestSuite extends TestSuite {
//...
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(ShoppingCartTest.suite());
return suite;
}
}
public class MyTestRunner {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(EcommerceTestSuite.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
}
}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class ShoppingCartTest {
private ShoppingCart bookCart;
private Product defaultBook;
//...
@Before
protected void setUp() {
bookCart = new ShoppingCart();
defaultBook = new Product("Extreme Programming", 23.95);
bookCart.addItem(defaultBook);
}
@After
protected void tearDown() {
bookCart = null;
}
@Test
public void testEmpty() {
bookCart.empty();
assertTrue(bookCart.isEmpty());
}
@Test
public void testProductAdd() {
Product book = new Product("Refactoring", 53.95);
bookCart.addItem(book);
double expectedBalance = defaultBook.getPrice() + book.getPrice();
assertEquals(expectedBalance, bookCart.getBalance(), 0.0);
assertEquals(2, bookCart.getItemCount());
}
@Test
public void testProductRemove() {
bookCart.removeItem(defaultBook);
assertEquals(0, bookCart.getItemCount());
assertEquals(0.0, bookCart.getBalance(), 0.0);
}
@Test(expected = ProductNotFoundException.class)
public void testProductNotFound() {
Product book = new Product("Ender's Game", 4.95);
bookCart.removeItem(book);
fail("Should raise a ProductNotFoundException");
}
}
public class EcommerceTestSuite extends TestSuite {
//...
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(ShoppingCartTest.suite());
suite.addTest(CreditCardTest.suite());
// etc.
return suite;
}
}
@RunWith(Suite.class)
@SuiteClasses({
ShoppingCartTest.class,
CreditCardTest.class
})
public class EcommerceTestSuite {
//...
}
¿Qué hemos conseguido con las anotaciones @Test del JDK
¿Qué hemos conseguido con las anotaciones @Test?
Diseñar y codificar una suite de casos de prueba unitaria para CreditCard usando jUnit versión 4.
En la arquitectura del framework se observan diversos patrones:


Colección de clases e interfaces que cooperan para formar un diseño reutilizable de un tipo específico de software
Abstracción
Máxima cohesión, mínimo acoplamiento
Coupling is the enemy of change, because it links together things that must change in parallel
D. Thomas & A. Hunt, The Pragmatic Programmer, 20th Anniversary Edition, 2019
La reducción de dependencias debe ser estratégica, es decir, centrada en los puntos del sistema que cambian con distinta frecuencia.
Si A y B cambian al mismo tiempo, no hay demasiado problema porque A
Si cambian con distinta frecuencia...
Una clase o módulo no debería configurar sus dependencias estáticamente, sino ser configurada desde fuera
Añadir pruebas unitarias al programa siguiente:
public class KnightOfTheRoundTable {
private String name;
private HolyGrailQuest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
public HolyGrail embarkOnQuest()
throws GrailNotFoundException {
return quest.embark();
}
}
public class HolyGrailQuest {
public HolyGrailQuest() {
/*...*/
}
public HolyGrail embark()
throws GrailNotFoundException {
HolyGrail grail = null;
// Look for grail ...
return grail;
}
}
¿Dónde está el acoplamiento?
import junit.framework.TestCase;
public class KnightOfTheRoundTableTest extends TestCase {
public void testEmbarkOnQuest() throws GrailNotFoundException {
KnightOfTheRoundTable knight =
new KnightOfTheRoundTable("CruzadoMagico");
HolyGrail grail = knight.embarkOnQuest();
assertNotNull(grail);
assertTrue(grail.isHoly());
}
}
¿Dónde está el acoplamiento?
No deseable
Instanciación de HolyGrail
Cada vez que se prueba KnightOfTheRoundTable, también se prueba HolyGrailQuest.
No se puede pedir a HolyGrailQuest que se comporte de otra forma (v.g. devolver null o elevar una excepción)
Ocultar la implementación detrás de una interfaz:
public interface Knight {
Object embarkOnQuest()
throws QuestFailedException;
}
public class KnightOfTheRoundTable
implements Knight {
private String name;
private Quest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
public Object embarkOnQuest()
throws QuestFailedException {
return quest.embark();
}
}
public interface Quest {
abstract Object embark()
throws QuestFailedException;
}
public class HolyGrailQuest implements Quest {
public HolyGrailQuest() { /*...*/ }
public Object embark()
throws QuestFailedException {
// Do whatever it means
// to embark on a quest
return new HolyGrail();
}
}
KnightOfTheRoundTable aún depende de un tipo específico de Quest (i.e. HolyGrailQuest) obtenido mediante new¿Debe ser el caballero responsable de obtener un desafío?
public class KnightOfTheRoundTable
implements Knight {
private String name;
private Quest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
}
public Object embarkOnQuest()
throws QuestFailedException {
return quest.embark();
}
public void setQuest(Quest quest) {
this.quest = quest;
}
}
El caballero sólo sabe del desafío a través de su interfaz Quest.
Puede asignársele cualquier implementación de Quest
(HolyGrailQuest, KillDragonQuest, etc.)
KnightOfTheRoundTable y HolyGrail porque embark() se ha definido como que devuelve un ObjectEjercicio: Discutir el tipo de retorno Object de embarkOnQuest:
ClassCastExceptionQuestEs la base de la inyección de dependencias
The question is: what aspect of control are they inverting? [...] Early user interfaces were controlled by the application program. You would have a sequence of commands like "Enter name", "enter address"; your program would drive the prompts and pick up a response to each one. With graphical (or even screen based) UIs the UI framework would contain this main loop and your program instead provided event handlers for the various fields on the screen. The main control of the program was inverted, moved away from you to the framework.
–– Martin Fowler, IoC containers and the DI pattern [1]
Una factoría proporciona un mecanismo de inyección de dependencias, visto desde el lado opuesto (los clientes adquieren las dependencias, no se les inyecta)
Ejemplo: Spring FactoryBean
We most likely would have been better off not attempting to create a reusable function in the first place
–– Roger Sessions, The Misuse of Reuse [2]
[2] http://simplearchitectures.blogspot.com.es/2012/07/misuse-of-reuse.html


Ahorro: Si

> [!NOTE] > En Java no se puede definir un método `default` en una `interface` que sea override‑equivalent a un método público de Object (como equals, hashCode, toString). Puedes declararlo de forma abstracta en la interfaz, pero no darle implementación default.
**Acoplamiento semántico**: A veces, dos componentes parecen idénticos, pero cambian por razones de negocio diferentes. Si los unificas para reutilizar, creas un acoplamiento semántico. En fases iniciales, es más barato duplicar que crear una mala abstracción, porque el código duplicado es fácil de borrar o cambiar independientemente, mientras que una abstracción incorrecta ata de manos a todo el sistema