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

  1. Use your code assistant’s IDE plugin to craft acceptance criteria into unit-test stubs
    1. “Given some initial context…”
    2. “When the user performs an action A…”
    3. “Then the system responds with B…”
  2. Implement the tests
  3. Have your code assistant write the code
  4. 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