So, what is this TDD business, anyway?
Remember the first day at the office?
/**
* This method will validate the given device association details.
* In case the validation fails, an exception will be thrown with a relevant error message.
* @param workspace - the shop workspace
* @param deviceAssociationDto - shop device DAO
* @param action - indicates which action type is handled
* @param accountId - the id of the account
* @param customerRefIdsForAccount
* @throws ValidationException
*/
private List<ValidationExceptionMessages> validateAction(Long workspace, DeviceAssociationDetailsDto deviceAssociationDto, DeviceAssociationSaveAction action, String accountId, List<String> customerRefIdsForAccount) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_FORMAT);
List<ValidationExceptionMessages> validationExceptionMessages = new ArrayList<>();
if (action != null && action.equals(DeviceAssociationSaveAction.RMA_ASSIGN_DEVICE)) {
return validationExceptionMessages;
}
if (StringUtils.isEmpty(deviceAssociationDto.getSerial())) {
validationExceptionMessages.add(ValidationExceptionMessages.SERIAL_IS_MANDATORY);
}
if(StringUtils.isNotEmpty(deviceAssociationDto.getSerial()) && !verifySerialExistInCustomer(accountId, deviceAssociationDto.getSerial(), customerRefIdsForAccount)){
validationExceptionMessages.add(ValidationExceptionMessages.INVALID_SERIAL);
}
// if (StringUtils.isEmpty(deviceAssociationDto.getShopCode()) && deviceAssociationDto.getSubRegionId()==null && deviceAssociationDto.getRegionId()==0) {
if (deviceAssociationDto.getEntityId() == 0) {
validationExceptionMessages.add(ValidationExceptionMessages.ILLEGAL_REGION); // At least region id should be filled
}
try {
if (StringUtils.isEmpty(deviceAssociationDto.getFrom())) {
validationExceptionMessages.add(ValidationExceptionMessages.FROM_DATE_MANDATORY);
} else if(!TimeUtils.isValidDateFormat(deviceAssociationDto.getFrom(), DATE_FORMAT)) {
validationExceptionMessages.add(ValidationExceptionMessages.INVALID_DATE_FORMAT);
} else if(action.equals(DeviceAssociationSaveAction.ADD_ASSIGNMENT_HISTORY)){
Date from = simpleDateFormat.parse(deviceAssociationDto.getFrom());
Date to = simpleDateFormat.parse(deviceAssociationDto.getTo());
if(!(from.before(new Date()) || from.equals(new Date()))) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_HISTORY_CAN_NOT_BE_IN_THE_FUTURE);
}
if (to.before(from)) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_HISTORY_TO_BEFORE_FROM);
}
}
// Partial mandatory fields validations, if mandatory field is missing --> no need to continue the validation
if (!validationExceptionMessages.isEmpty()) {
return validationExceptionMessages;
}
CustomerElementType elementType = CustomerElementType.valueOf(deviceAssociationDto.getAssignmentEntityType());
if(elementType.equals(CustomerElementType.REGION) && deviceAssociationDto.getEntityId()==0) {
validationExceptionMessages.add(ValidationExceptionMessages.ILLEGAL_REGION);
} else if(elementType.equals(CustomerElementType.SUB_REGION) && deviceAssociationDto.getEntityId()==0) {
// Long shopIdByCode = organizationStructureService.getShopIdByCode(deviceAssociationDto.getShopCode(), workspace);
// if (StringUtils.isNotEmpty(deviceAssociationDto.getShopCode()) && (shopIdByCode == null)) {
validationExceptionMessages.add(ValidationExceptionMessages.SHOP_CODE_NOT_EXIST);
// }
}
DeviceDto device = deviceService.getDeviceBySerial(deviceAssociationDto.getSerial());
if (device == null) {
validationExceptionMessages.add(ValidationExceptionMessages.DEVICE_ASSOCIATION_NOT_EXIST);
}
if(action.equals(DeviceAssociationSaveAction.ADD_ASSIGNMENT_HISTORY)){
if(StringUtils.isEmpty(deviceAssociationDto.getTo())) {
validationExceptionMessages.add(ValidationExceptionMessages.TO_DATE_MANDATORY);
} else if(!TimeUtils.isValidDateFormat(deviceAssociationDto.getTo(), DATE_FORMAT)) {
validationExceptionMessages.add(ValidationExceptionMessages.INVALID_DATE_FORMAT);
}else {
Date to = null;
if (StringUtils.isNotEmpty(deviceAssociationDto.getTo())) {
to = simpleDateFormat.parse(deviceAssociationDto.getTo());
}
if(!(to.before(new Date()) || to.equals(new Date()))) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_HISTORY_CAN_NOT_BE_IN_THE_FUTURE);
}
}
}
List<DeviceAssociation> deviceAssociationHistories = accountDeviceDao.getDeviceAssociationHistory(accountId, deviceAssociationDto.getSerial());
if (CollectionUtils.isNotEmpty(deviceAssociationHistories)) {
Date from = simpleDateFormat.parse(deviceAssociationDto.getFrom());
Date to = null;
if (StringUtils.isNotEmpty(deviceAssociationDto.getTo())) {
to = simpleDateFormat.parse(deviceAssociationDto.getTo());
}
for (DeviceAssociation deviceAssociationHistory : deviceAssociationHistories) {
// Avoid comparing range to itself
if(deviceAssociationHistory.getId() == deviceAssociationDto.getId()) {
continue;
}
// Verify there is no conflict with current shop assignment
if(deviceAssociationHistory.getToDate()==null && to!=null) {
// [from]=currentAssignmentFrom
if (from.equals(deviceAssociationHistory.getFromDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
// [to]>=currentAssignmentFrom
if (to.equals(deviceAssociationHistory.getFromDate()) || to.after(deviceAssociationHistory.getFromDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
}
// [from]=rangeFrom or [from]=rangeTo
if (from.equals(deviceAssociationHistory.getFromDate()) || from.equals(deviceAssociationHistory.getToDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
// Verify there is no conflict with history shop assignment
if (deviceAssociationHistory.getToDate() != null && to != null) {
// [to]=rangeFrom or [to]=rangeTo
if (to.equals(deviceAssociationHistory.getFromDate()) || to.equals(deviceAssociationHistory.getToDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
// rangeFrom < [from] < rangeTo
if (from.after(deviceAssociationHistory.getFromDate()) && from.before(deviceAssociationHistory.getToDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
// rangeFrom < [to] < rangeTo
if (to.after(deviceAssociationHistory.getFromDate()) && to.before(deviceAssociationHistory.getToDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
// [from] < rangeFrom and rangeTo < [to]
if (from.before(deviceAssociationHistory.getFromDate()) && to.after(deviceAssociationHistory.getToDate())) {
validationExceptionMessages.add(ValidationExceptionMessages.ASSIGNMENT_ALREADY_EXIST_FOR_DEVICE);
}
}
}
}
} catch (ParseException e) {
e.printStackTrace();
}
return validationExceptionMessages;
}154 lines of code in a single method...
AccountDeviceServiceImpl.java
Or...
Got a bug
Knew exactly where the issue was

Only to realize...


Developers Don't Write Tests!
- It's boring
- Writing after the code seems moot
- It's hard to write tests...espcially if you did not plan for them
EVEN IF THEY AGREE THEY ARE IMPORTANT
SO, WHAT IS TDD?

“Why do most developers fear making continuous changes to their code? They are afraid they’ll break it! Why are they afraid they’ll break it? Because they don’t have tests.”
~ Robert C Martin (Clean Code)


- Not about testing...well...not only about them
-
it's more about designing with tests.
-
- About simple design
- Inspires confidence
- It Promotes
- KISS
- Keep It Stupid Simple
- YAGNI
- You Ain't Gonna Need It
So...
Collaboration
Unit Tests
Integration tests
e2e
mocks
fakes
Test doubles
stubs
refactoring
contract tests
example/properties tests
mocking
spys
dummy objects
Acceptance testing
Behavior-driven development (BDD)
Continuous testing
Happy Path
The What
- We get Better Design
- Safer Refactoring
- Better Code Coverage
- Faster debugging
- Self documenting tests
The How
- Cleaner Code (Refactoring)
- Increased Quality
- Most often, the failing test is the most recently changed
- Tests showcase how to use the code
TDD helps with but does not guarantee, good design & good code. Skill, talent, and expertise remain necessary
The 3 rules of TDD
- 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

Best Practices for TDD
-
Keep tests small and focused.
-
Use meaningful names.
-
Test behavior, not implementation
-
Critical for UI testing
-
-
Maintain a fast test suite.
-
Don’t skip the "Refactor" step!
To Recap: The values of TDD
-
Baby steps - instead of large-scale changes
-
Continuous refactoring - instead of late quality improvements
-
Evolutionary design - instead of big upfront
-
Executable documentation - instead of static documents
-
Minimalist code - instead of a gold-plated solution
Reading materials


TDD with AI
- Use your code assistant’s IDE plugin to craft acceptance criteria into unit-test stubs
- “Given some initial context…”
- “When the user performs an action A…”
- “Then the system responds with B…”
- Implement the tests
- Have your code assistant write the code
- You or your AI can now refactor the code (Remember even if the AI "does not" need clean code, the developers are still in the mix).

Let's code
TDD
By Eyal Mrejen
TDD
- 242