Crafting Reliable

Testable Code

Doguhan Uluca

Technical Fellow  at

@duluca

&

with TypeScript

value

quality

Features

Time

Quality

Roy Osherove

Lean

Kanban

Scrum

LeSS

SAFe

SoS

Nexus

Spotify

, forward flow

consistent

of features

"It's not up to them

it's up to us [the developers]"

Ron Jeffries, Chet Hendrickson, deliver:Agile 2018.

  • know when and how to say no

  • it's up to you to deliver a quality product

Reliable Engineering Best Practices

  • High cohesion & Low coupling
  • DRY
  • KISS
  • AC/DC
  • Self-documenting code
  • Unit Testing
  • SOLID
  • Stateless & Data-Driven
  • Shift complexity to the back
  • Iterative & Incremental
  • Embrace reactive coding
  • JavaScript at scale
  • Development time tool
  • Optional type checking
  • Add types to JavaScript libraries
  • Supports OOP & Functional Concepts
  • Generics & interfaces
  • Covers the JavaScript Feature Gap

Benefits of TypeScript

Past

Present

Future

ES3

ES5

ES2015 (aka ES6)

2015

ES3 & ES5

ES2015 - ES2016

ES2017

2017

ES3 & ES 5

ES2019

2019

ES2021

2021

ESPast

ESCurrent

ESNext

Current

ES2015 - ES2017

ES2015 - ES2020

ES3 & ES 5

JavaScript Feature Gap

Sample Project

High-Level Goals

High Cohesion - Low coupling

  • Highly cohesion
    • Keep code that belongs together, together
  • Low coupling
    • Don't expose inner workings of file/class/function

High Cohesion

lemon-mart-server
├───bin
├───web-app (default Angular setup)
├───server
│ ├───src
│ │ ├───models
│ │ ├───public
│ │ ├───services
│ │ ├───v1
│ │ │ └───routes
│ │ └───v2
│ │ └───routes
│ └───tests
│ package.json
│ README.md

Low Coupling

export class LemonRaterComponent
   implements ControlValueAccessor, AfterViewInit {
     
  disabled = false

  internalValue!: number
  ...
}

Low Coupling

export class LemonRaterComponent {
  private internalValue!: number
  
  get value() {
    return this.internalValue
  }

  setRating(lemon: any) {
    if (!this.disabled) {
      this.internalValue = lemon.value
      this.setDisplayText()
      this.onChanged(lemon.value)
      this.onTouched()
    }
  }

  setDisplayText() {
    this.setSelectedText(this.internalValue)
  }
}

DRY

Don't repeat yourself!

Don't repeat yourself!

Don't repeat yourself!

KISS

keep it simple, stupid!

AC/DC

Avoid complexity, depend (on your own) code

DRY, KISS, AC/DC

  • Export reusable functions
  • Avoid libraries
  • Avoid overusing dependency injection
new FormControl('', 
  [Validators.required, Validators.minLength(2)]
)

DRY, KISS, AC/DC

duluca/lemon-mart/common/validations.ts

export const OptionalTextValidation = 
  [Validators.minLength(2), Validators.maxLength(50)]
export const RequiredTextValidation = 
  OptionalTextValidation.concat([Validators.required])
export const EmailValidation = [Validators.required, Validators.email]
export const PasswordValidation = ...
export const BirthDateValidation = [
  Validators.required,
  Validators.min(new Date().getFullYear() - 100),
  Validators.max(new Date().getFullYear()),
]
export const USAZipCodeValidation = [
  Validators.required,
  Validators.pattern(/^\d{5}(?:[-\s]\d{4})?$/),
]
export const USAPhoneNumberValidation = ...

DRY, KISS, AC/DC

duluca/lemon-mart-server/server/services/authService.ts

export function createJwt(user: IUser): Promise<string> {
 ...
 sanitizeToken(token)
}

export function authenticate(options?: {
  requiredRole?: Role
  permitIfSelf?: {
    idGetter: (req: Request) => string
    requiredRoleCanOverride: boolean
  }
}) {
  ...
}

function sanitizeToken(authorization: string | undefined) {
 ...
}

DRY, KISS, AC/DC

duluca/lemon-mart-server/server/routes/userRouter.ts

router.get(
  '/',
  authenticate({ requiredRole: Role.Manager }),
  async (req: Request, res: Response) => {
    const query: Partial<IQueryParameters> = {
      filter: req.query.filter,
      limit: req.query.limit,
      skip: req.query.skip,
      sortKeyOrList: req.query.sortKey,
      projectionKeyOrList: ['email', 'role', '_id', 'name'],
    }

    const users = await UserCollection.findWithPagination<User>(query)
    res.send(users)
  }
)

DRY, KISS, AC/DC

Self-documenting code

  • Code is written once, read many times
  • Once written, documentation is never updated nor read
  • Verbose
  • Living, useful documentation
  • No string literals
  • No string literals
  • No string literals
duluca/lemon-mart-server/server/routes/userRouter.ts

/**
 * @swagger
 * components:
 *   parameters:
 *     filterParam:
 *       in: query
 *       name: filter
 *       required: false
 *       schema:
 *         type: string
 *       description: Search text to filter the result set by
 *     skipParam:
 *       in: query
 *       name: skip
 *       required: false
 *       schema:
 *         type: integer
 *         minimum: 0
 *       description: The number of items to skip before collecting the result set.
 *     limitParam:
 *       in: query
 *       name: limit
 *       required: false
 *       schema:
 *         type: integer
 *         minimum: 1
 *         maximum: 50
 *         default: 10
 *       description: The numbers of items to return.
 *     sortKeyParam:
 *       in: query
 *       name: sortKey
 *       required: false
 *       schema:
 *         type: string
 *       description: Name of a column to sort ascending.
 *                    Prepend column name with a dash to sort descending.
 */

/**
 * @swagger
 * /v2/users:
 *   get:
 *     description: |
 *       Searches, sorts, paginates and returns a summary of `User` objects.
 *     parameters:
 *       - $ref: '#/components/parameters/filterParam'
 *       - $ref: '#/components/parameters/skipParam'
 *       - $ref: '#/components/parameters/limitParam'
 *       - $ref: '#/components/parameters/sortKeyParam'
 *     responses:
 *       "200": # Response
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 total:
 *                   type: integer
 *                 data:
 *                   type: array
 *                   items:
 *                     type: object
 *                     properties:
 *                       _id:
 *                         type: string
 *                       email:
 *                         type: string
 *                       fullName:
 *                         type: string
 *                       name:
 *                         $ref: "#/components/schemas/Name"
 *                       role:
 *                         $ref: "#/components/schemas/Role"
 *                     description: Summary of `User` object.
 */
router.get(
  '/',
  authenticate({ requiredRole: Role.Manager }),
  async (req: Request, res: Response) => {
    const query: Partial<IQueryParameters> = {
      filter: req.query.filter,
      limit: req.query.limit,
      skip: req.query.skip,
      sortKeyOrList: req.query.sortKey,
      projectionKeyOrList: ['email', 'role', '_id', 'name'],
    }

    const users = await UserCollection.findWithPagination<User>(query)
    res.send(users)
  }
)
Self-Documenting Code
duluca/lemon-mart-server/server/routes/userRouter.ts

/**
 * @swagger
 * components:
 *   parameters:
 *     filterParam:
 *       in: query
 *       name: filter
 *       required: false
 *       schema:
 *         type: string
 *       description: Search text to filter the result set by
 *     skipParam:
 *       in: query
 *       name: skip
 *       required: false
 *       schema:
 *         type: integer
 *         minimum: 0
 *       description: The number of items to skip before collecting the result set.
 *     limitParam:
 *       in: query
 *       name: limit
 *       required: false
 *       schema:
 *         type: integer
 *         minimum: 1
 *         maximum: 50
 *         default: 10
 *       description: The numbers of items to return.
 *     sortKeyParam:
 *       in: query
 *       name: sortKey
 *       required: false
 *       schema:
 *         type: string
 *       description: Name of a column to sort ascending.
 *                    Prepend column name with a dash to sort descending.
 */

/**
 * @swagger
 * /v2/users:
 *   get:
 *     description: |
 *       Searches, sorts, paginates and returns a summary of `User` objects.
 *     parameters:
 *       - $ref: '#/components/parameters/filterParam'
 *       - $ref: '#/components/parameters/skipParam'
 *       - $ref: '#/components/parameters/limitParam'
 *       - $ref: '#/components/parameters/sortKeyParam'
 *     responses:
 *       "200": # Response
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 total:
 *                   type: integer
 *                 data:
 *                   type: array
 *                   items:
 *                     type: object
 *                     properties:
 *                       _id:
 *                         type: string
 *                       email:
 *                         type: string
 *                       fullName:
 *                         type: string
 *                       name:
 *                         $ref: "#/components/schemas/Name"
 *                       role:
 *                         $ref: "#/components/schemas/Role"
 *                     description: Summary of `User` object.
 */
router.get(
  '/',
  authenticate({ requiredRole: Role.Manager }),
  async (req: Request, res: Response) => {
    const query: Partial<IQueryParameters> = {
      filter: req.query.filter,
      limit: req.query.limit,
      skip: req.query.skip,
      sortKeyOrList: req.query.sortKey,
      projectionKeyOrList: ['email', 'role', '_id', 'name'],
    }

    const users = await UserCollection.findWithPagination<User>(query)
    res.send(users)
  }
)
Self-Documenting Code
duluca/document-ts/collectionFactory.ts

private cursor<T>(
    cursor: AggregationCursor<T> | undefined,
    params: Partial<QueryParameters> & object,
    query: {} | undefined
  ): AggregationCursor<T> | Cursor<K> {
    if (cursor) {
      if (params && params.filter) {
        cursor = cursor.match(
          this.query(params.filter, this.props)
        )
      }
      return cursor
    } else {
      let projection: object[] = []

      if (params && params.projectionKeyOrList) {
        projection = this.convertToObject(params.projectionKeyOrList, 0)
      }

      return this.cursor(query, Object.assign({}, ...projection))
    }
  }

Readable Code

duluca/document-ts/collectionFactory.ts

private buildCursor<TReturnType>(
    aggregationCursor: AggregationCursor<TReturnType> | undefined,
    queryParams: Partial<IQueryParameters> & object,
    builtQuery: {} | undefined
  ): AggregationCursor<TReturnType> | Cursor<TDocument> {
    if (aggregationCursor) {
      if (queryParams && queryParams.filter) {
        aggregationCursor = aggregationCursor.match(
          this.buildTokenizedQueryObject(queryParams.filter, this.searchableProperties)
        )
      }
      return aggregationCursor
    } else {

      let projection: object[] = []

      if (queryParams && queryParams.projectionKeyOrList) {
        projection = this.convertKeyOrListToObject(queryParams.projectionKeyOrList, 0)
      }

      return this.getCursor(builtQuery, Object.assign({}, ...projection))
    }
  }

Readable Code

  • No string literals in code
  • No string literals in code
  • No string literals in code

Use Enums

duluca/lemon-mart/auth/role.enum.ts

export enum Role {
  None = 'none',
  Clerk = 'clerk',
  Cashier = 'cashier',
  Manager = 'manager',
}

TypeScript helps us write

readable, DRY, and simple code

Unit Testing

Test Driven Development

  • Forces deeper thought about design
  • Ensures testable code is written
  • Avoids over-engineering

Test Driven Development

  1. Red
  2. Green
  3. Refactor
  4. Repeat

FIRST principle

  • Fast
  • Isolated
  • Repeatable
  • Self-verifying
  • Timely

Mike Cohn's Testing Pyramid

Keep it organized & focused

  • Class Under Test
  • File Under Test
  • Function Test

Anatomy of a unit test

  • Arrange – setup
  • Act – run the thing you want to test
  • Assert – verify the results
duluca/local-weather-app/weather/weather.service.spec.ts

describe('getCurrentWeather', () => {
    it('should return value given zip code', () => {
      // Arrange
      const httpMock = TestBed.inject(HttpTestingController)
      const uriParams = 'lat=38.887103&lon=-77.093197'
      postalCodeServiceMock.resolvePostalCode.and.returnValue(
        of({
          postalCode: '22201',
          lat: 38.887103,
          lng: -77.093197,
          countryCode: 'US',
          placeName: 'Arlington',
        } as IPostalCode)
      )

      // Act
      weatherService.getCurrentWeather('22201').subscribe()

      // Assert
      const request = httpMock.expectOne(
        `${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
          `${uriParams}&appid=01ff1417eeb4a81b09ac68b15958d453`,
        'call to api'
      )

      expect(request.request.method).toBe('GET')
    })
  })

Arrange, Act, Assert

Isolating Dependencies

weatherApi

weatherComponent

api.openweathermap.org/data/2.5/weather

weatherServiceMock

getCurrentWeather
return(32)
api.openweathermap.org/data/2.5/weather
hasBeenCalled(1)

weatherService

getCurrentWeather

Test Harness

duluca/local-weather-app/weather/weather.service.spec.ts

describe('getCurrentWeather', () => {
    it('should return value given zip code', () => {
      // Arrange
      const httpMock = TestBed.inject(HttpTestingController)
      const uriParams = 'lat=38.887103&lon=-77.093197'
      postalCodeServiceMock.resolvePostalCode.and.returnValue(
        of({
          postalCode: '22201',
          lat: 38.887103,
          lng: -77.093197,
          countryCode: 'US',
          placeName: 'Arlington',
        } as IPostalCode)
      )

      // Act
      weatherService.getCurrentWeather('22201').subscribe()

      // Assert
      const request = httpMock.expectOne(
        `${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
          `${uriParams}&appid=01ff1417eeb4a81b09ac68b15958d453`,
        'call to api'
      )

      expect(request.request.method).toBe('GET')
    })
  })

Mocks and Spies

TypeScript helps us write

precise unit tests

SOLID

  • Single Responsibility
    • DRY, one place to change
  • Open-Closed
    • open for extension, closed for modification
  • Liskov Substitution
    • Use objects derived from base/abstract classes without knowing about specific implementation
  • Interface segregation
    • Many specific interfaces over one general-purpose interface
  • Dependency inversion
    • Depend on abstractions, not concretions

Single Responsibility Principle

export class LemonRaterComponent {
  internalValue!: number

  setRating(lemon: any) {
    let text = ''

    if (lemon.value) {
      text = this.ratings.find((i) => i.value === value)?.text || ''
    }
      
    if (!this.disabled) {
      this.internalValue = lemon.value
      this.displayTextRef.nativeElement.textContent = text
      this.onChanged(lemon.value)
      this.onTouched()
    }
  }

  setDisplayText() {
    let text = ''

    if (this.internalValue) {
      this.displayTextRef.nativeElement.textContent = 
        this.ratings.find((i) => i.value === value)?.text || ''
    }
  }
}

Single Responsibility Principle

export class LemonRaterComponent {
  private internalValue!: number
  
  get value() {
    return this.internalValue
  }

  setRating(lemon: any) {
    if (!this.disabled) {
      this.internalValue = lemon.value
      this.setDisplayText()
      this.onChanged(lemon.value)
      this.onTouched()
    }
  }

  setDisplayText() {
    this.setSelectedText(this.internalValue)
  }

  private setSelectedText(value: number) {
    this.displayTextRef.nativeElement.textContent = 
      this.getSelectedText(value)
  }

  private getSelectedText(value: number) {
    let text = ''

    if (value) {
      text = this.ratings.find((i) => i.value === value)?.text || ''
    }

    return text
  }
}

Open-Closed

Principle

Open-Closed

Principle

duluca/lemon-mart/src/app/auth/auth.service.ts

export interface IAuthService {
  readonly authStatus$: BehaviorSubject<IAuthStatus>
  readonly currentUser$: BehaviorSubject<IUser>
  login(email: string, password: string): Observable<void>
  logout(clearToken?: boolean): void
  getToken(): string
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.service.ts

export abstract class AuthService implements IAuthService {
  authStatus$: BehaviorSubject<IAuthStatus>
  currentUser$: BehaviorSubject<IUser>
  
  constructor() {}
  
  login(email: string, password: string): Observable<void> {
    throw new Error('Method not implemented.')
  }
    
  logout(clearToken?: boolean): void {
    throw new Error('Method not implemented.')
  }

  getToken(): string {
    throw new Error('Method not implemented.')
  }
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.service.ts

export abstract class AuthService implements IAuthService {
  protected abstract authProvider(
    email: string,
    password: string
  ): Observable<IServerAuthResponse>
  
  protected abstract transformJwtToken(token: unknown): IAuthStatus
  protected abstract getCurrentUser(): Observable<User>
  ...
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.service.ts

login(email: string, password: string): Observable<void> {
  this.clearToken()

  const loginResponse$ = this.authProvider(email, password).pipe(
    map((value) => {
      this.setToken(value.accessToken)
      const token = decode(value.accessToken)
      return this.transformJwtToken(token)
    }),
    tap((status) => this.authStatus$.next(status)),
    this.getAndUpdateUserIfAuthenticated
  )

  loginResponse$.subscribe({
    error: (err) => {
      this.logout()
      return throwError(err)
    },
  })

  return loginResponse$
}

Liskov Subsitution

duluca/lemon-mart/src/app/auth/auth.inmemory.service.ts

export class InMemoryAuthService extends AuthService {
  // LemonMart Server User Id: 5da01751da27cc462d265913
  private defaultUser = User.Build({...})

  constructor() {
    super()
    console.warn(
      "You're using the InMemoryAuthService. Do not use this service in production."
    )
  }

  protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
    const authStatus = { isAuthenticated: true, ... } as IAuthStatus

    const authResponse = {
      accessToken: sign(authStatus, 'secret', {
        expiresIn: '1h',
        algorithm: 'none',
      }),
    } as IServerAuthResponse

    return of(authResponse)
  }

  protected transformJwtToken(token: IAuthStatus): IAuthStatus {
    return token
  }

  protected getCurrentUser(): Observable<User> {
    return of(this.defaultUser)
  }
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.firebase.service.ts

@Injectable()
export class FirebaseAuthService extends AuthService {
  constructor(private afAuth: AngularFireAuth) {
    super()
  }

  protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
    return this.afAuth.signInWithEmailAndPassword(email, password)
  }

  protected transformJwtToken(token: IJwtToken): IAuthStatus {
    return { isAuthenticated: token.email ? true : false, userId: token.sub, userRole: Role.None }
  }

  protected getCurrentUser(): Observable<User> {
    return this.afAuth.user.pipe(map(this.transformFirebaseUser))
  }

  logout() {
    if (this.afAuth) {
      this.afAuth.signOut()
    }
    this.clearToken()
    this.authStatus$.next(defaultAuthStatus)
  }
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.custom.service.ts

export class CustomAuthService extends AuthService {
  constructor(private httpClient: HttpClient) {
    super()
  }

  protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
    return this.httpClient.post<IServerAuthResponse>(
      `${environment.baseUrl}/v1/auth/login`, { email, password }
    )
  }

  protected transformJwtToken(token: IJwtToken): IAuthStatus {
    return {
      isAuthenticated: token.email ? true : false,
      userId: token.sub,
      userRole: $enum(Role).asValueOrDefault(token.role, Role.None),
      userEmail: token.email,
      userPicture: token.picture,
    } as IAuthStatus
  }

  protected getCurrentUser(): Observable<User> {
    return this.httpClient
      .get<IUser>(`${environment.baseUrl}/v1/auth/me`)
      .pipe(map(User.Build, catchError(transformError)))
  }
}

Open-Closed Principle

duluca/lemon-mart/src/app/auth/auth.factory.ts

export function authFactory(afAuth: AngularFireAuth, httpClient: HttpClient) {
  switch (environment.authMode) {
    case AuthMode.InMemory:
      return new InMemoryAuthService()
    case AuthMode.CustomServer:
      return new CustomAuthService(httpClient)
    case AuthMode.Firebase:
      return new FirebaseAuthService(afAuth)
  }
}
duluca/lemon-mart/src/app/app.module.ts

providers: [
  {
    provide: AuthService,
    useFactory: authFactory,
    deps: [AngularFireAuth, HttpClient],
  }
]

Dependency Inversion

duluca/lemon-mart/src/app/user/user/user.service.ts

getUser(id): Observable<IUser> {
  return this.httpClient.get<IUser>(`${environment.baseUrl}/v1/user/${id}`)
}
duluca/local-weather-app/src/app/weather/weather.service.ts

private getCurrentWeatherHelper(uriParams: string): 
  Observable<ICurrentWeather> {
  return this.httpClient
    .get<ICurrentWeatherData>(
      `${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
        `${uriParams}&appid=${environment.appId}`
    )
    .pipe(map(data => this.transformToICurrentWeather(data)))
}

Interface Segregation

"To write unit-testable code, be sure to adhere to the Single Responsibility and
Open/Closed principles of the SOLID principles."

Doguhan Uluca, Angular for Enterprise-Ready Web Apps, 2020.

TypeScript enables SOLID code

  • Refactor code, so you can export reusable functions
  • Pass around abstractions (i.e. interfaces)
  • Use enums instead of string literals
  • Use interfaces to document the shape of your data (internal or external)
  • Use classes to reuse context-specific behavior
  • Use abstract base classes to enforce implementation patterns, reuse context-specific behavior 

Manage Complexity

  • Documents shape of data
  • Pass abstractions, not concretions
  • Separate internal data shape from external shape

Interfaces

User Interface

duluca/lemon-mart/user/user.ts
export interface IUser {
  id: string
  email: string
  name: IName
  …
  address: {
    line1: string
    line2: string
    city: string
    state: string
    zip: string
  }
  phones: IPhone[]
}

export interface IName {
  first: string
  middle?: string
  last: string
}
export interface IPhone {
  type: string
  number: string
  id: number
}

export class User implements IUser
  • Aim for a flat data hierarchy
    • Flatten complicated data structures in API and service layer
  • Arrays and simple shapes for common objects are okay
    • i.e. a name object or a common domain-specific object
  • ​Prepend interface types with a capital "i"
    • i.e. IName vs Name

Interface Tips

Keep Complexity in the Backend

User Document Interface

duluca/lemon-mart-server/models/user.ts
export interface IUser extends IDocument {
  email: string
  name: IName
  picture: string
  role: Role
  userStatus: boolean
  dateOfBirth: Date
  level: number
  address: {
    line1: string
    line2?: string
    city: string
    state: string
    zip: string
  }
  phones?: IPhone[]
}
import * as bcrypt from 'bcryptjs'
import { CollectionFactory, Document, IDocument } 
  from 'document-ts'
import { AggregationCursor, ObjectID } 
  from 'mongodb'
import { v4 as uuid } from 'uuid'

Don't Share Models

User Model

duluca/lemon-mart-server/models/user.ts

private setPassword(newPassword: string): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) {
        return reject(err)
      }
      bcrypt.hash(newPassword, salt, (hashError, hash) => {
        if (hashError) {
          return reject(hashError)
        }
        resolve(hash)
      })
    })
  })
}

comparePassword(password: string): Promise<boolean> {
  const user = this
  return new Promise((resolve, reject) => {
    bcrypt.compare(password, user.password, (err, isMatch) => {
      if (err) {
        return reject(err)
      }
      resolve(isMatch)
    })
  })
}

Move complexity to the backend

  • Move business logic to the backend
  • Flatten data hierarchy
  • Consider GraphQL (when appropriate)

User Model

public get fullName(): string {
  if (this.name.middle) {
    return `${this.name.first} ${this.name.middle} ${this.name.last}`
  }
  return `${this.name.first} ${this.name.last}`
}

Summary View

Stateless Code

  • No class properties
  • No global variables
  • Data-driven design

What you avoid implementing is more important than what you implement

Source: A wise developer

TypeScript helps us

manage complexity

Iterative & Incremental

Authenticate to Authorize

implement a Login experience

achieved iteratively and incrementally

STATELESS

COMPOSABLE

REACTIVE

  1. Put water into the pipes
  2. Direct water into the heater
  3. Make an API call to the utility company to turn on power
  4. Turn on the heater
  5. Wait for water to heat
  6. Turn on the faucet for hot water
  7. Don't forget to undo your steps, when done

Imperative

  1. Turn on/off the faucet for hot water

Reactive

Reactive

products$ = this.http.get<Product[]>(this.productsUrl)
    .pipe(
      tap(data => console.log('getProducts: ', JSON.stringify(data))),
      shareReplay(),
      catchError(this.handleError)
    )

Reactive

productsWithCategory$ = combineLatest(
    this.products$,
    this.productCategoryService.productCategories$
  ).pipe(
    map(([products, categories]) =>
      products.map(
        p =>
          ({
            ...p,
            category: categories.find(c => p.categoryId === c.id).name
          } as Product) // <-- note the type here!
      )
    ),
    shareReplay()
  )

TypeScript makes it easy to be

stateless, composable, and reactive

 

 

Reliable Engineering Best Practices

  • High cohesion & Low coupling
  • DRY
  • KISS
  • AC/DC
  • Self-documenting code
  • Unit Testing
  • SOLID
  • Stateless & Data-Driven
  • Shift complexity to the back
  • Iterative & Incremental
  • Embrace reactive coding

Write unit testable code

Keep it simple

Manage complexity

Be reactive, iterative, incremental

We're hiring, including fully remote positions!

DRY

  • Don't Repeat Yourself
    • Refactor code into sharable functions
      • Import functions in other files
    • OOP
      • Abstract classes
      • Inheritance

Object-Oriented Design

  • Move behavior to classes
  • i.e. hydration, toJSON, calculated properties
  • Don’t abuse OOP
    • Avoid state inside classes, remain functional

Behavior in Classes

duluca/lemon-mart/src/app/user/user/user.ts
export class User implements IUser {
  static Build(user: IUser) {
    return new User(
      user.id,
      user.email,
      user.name,
      …
    )
  }

  get fullName() {
    return this.name ? 
      `${this.name.first} ${this.name.middle} ${this.name.last}` : ''
  }

  toJSON() {
    return JSON.stringify(...)
  }
}
duluca/lemon-mart/src/app/common/base-form.class.ts
export abstract class BaseFormComponent<TFormData> {
  @Input() initialData: TFormData
  @Input() disable: boolean
  @Output() formReady: EventEmitter<AbstractControl>
  formGroup: FormGroup

  private registeredForms: string[] = []

  constructor() {
    this.formReady = new EventEmitter<AbstractControl>(true)
  }

  abstract buildForm(initialData?: TFormData): FormGroup

  patchUpdatedData(data) {
    this.formGroup.patchValue(data, { onlySelf: false })
  }
  …
}

SOLID in action

duluca/lemon-mart/src/app/common/base-form.class.ts
export class NameInputComponent extends BaseFormComponent<IName>
  implements OnInit, OnChanges {
  constructor(private formBuilder: FormBuilder) {
    super()
  }

  buildForm(initialData?: IName): FormGroup {
    const name = initialData
    return this.formBuilder.group({ … })
  }

  ngOnInit() {
    this.formGroup = this.buildForm(this.initialData)
    this.formReady.emit(this.formGroup)
  }
  …
}

SOLID in action

CI/CD

Types of Tests

  • Unit
  • Component
  • System
  • Functional
  • Acceptance
  • Performance
  • Security
  • Exploratory
  • Usability

Code

Test

Deploy

Publish

repeat

Branch

Merge

GitHub Flow

  • enforce standards

  • isolate changes

  • make it repeatable

.circleci/config.yml

.circleci/config.yml

FROM
jobs
build
deploy

.circleci/config.yml

FROM
build
env
steps

Docker

checkout

run

store build artifacts

store test results

  • Use declarative tools

  • Achieve Infrastructure-as-Code

  • Repeatable Integration

CI/CD

Infrastructure-as-Code closes the Configuration Gap

Dockerfile

Anatomy of a Dockerfile

FROM
SETUP
COPY
CMD

Security

Dependencies

npm Scripts for Docker

bit.ly/npmScriptsForDocker

  • Create living documentation

  • Achieve DevOps

  • Close the configuration gap

n ➡️ n Deployments

Local build

CI-x build

...

.circleci/config.yml

FROM
jobs
build
deploy

.circleci/config.yml

FROM
deploy
restore
steps

 Build artifact

Sign in to container repo

Publish to repo

Trigger deployment

npm Script for AWS

bit.ly/npmScriptsForAWS

npm run aws:fargate-release
heroku container:push web -a lemon-mart
gcloud beta run deploy --image lemon-mart

Google Cloud Run

  • Use declarative tools

  • Achieve Infrastructure-as-Code

  • Repeatable deployments

  • n ➡️ n Flexibility

describe('Converters', () => {
  describe('convertCtoF', () => {
    it('should convert 0c to 32f', () => {
      ...
    })
  })
})

Sample Jasmine Test

Converters convertCtoF should convert 0c to 32f

describe('Converters', () => {
  beforeAll(() => { ... })
  
  beforeEach(() => { ... })
  
  describe('convertCtoF', () => {
    it('should convert 0c to 32f', () => {
      ...
    })
  })
  describe('convertFtoC', () => {
    it('should convert 32f to 0c', () => {
      ...
    })
  })
  
  afterEach(() => { ... })
  
  afterAll(() => { ... })
})

Fixtures

Fixtures are scoped within describe blocks

 
  describe('convertCtoF', () => {
    it('should convert 0c to 32f', () => {
      // Act
      
      // Arrange
      
      // Assert
      fail('message')
      expect(expected).toBe(actual)
                      .toEqual(actual)
                      .toBeDefined()
                      .toBeFalsy()
                      .toThrow(exception)
                      .nothing()
    })
  })

Matchers

Test doubles

  • Fakes
  • Mocks, stubs, or spies

Test doubles

duluca/local-weather-app/weather/weather.service.spec.ts

describe('getCurrentWeather', () => {
    it('should return value given zip code', () => {
      // Arrange
      const httpMock = TestBed.inject(HttpTestingController)
      const uriParams = 'lat=38.887103&lon=-77.093197'
      postalCodeServiceMock.resolvePostalCode.and.returnValue(
        of({
          postalCode: '22201',
          lat: 38.887103,
          lng: -77.093197,
          countryCode: 'US',
          placeName: 'Arlington',
        } as IPostalCode)
      )

      // Act
      weatherService.getCurrentWeather('22201').subscribe()

      // Assert
      const request = httpMock.expectOne(
        `${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
          `${uriParams}&appid=01ff1417eeb4a81b09ac68b15958d453`,
        'call to api'
      )

      expect(request.request.method).toBe('GET')
    })
  })

Test Doubles

Test doubles

fakes

weatherApi -> wweatherService -> weatherComponent

Angular

app.ts

rootRouter

services

pipes

modules

/a: default

/master

/detail

/b/...

/c

childRouter

/d

/e

/f

Angular

weatherApi

weatherService

weatherComponent

weatherServiceFake

weatherComponent

weatherApiFake?

weatherService

50 services, 50 fakes?

getCurrentWeather
api.openweathermap.org/data/2.5/weather