Fireproof components

Who Am I?

  • Full Stack Developer at Lean Mind
  • Passionate about development
  • Typescript lover 💙

Adrián Ferrera González

@afergon

adrian-afergon

What we will talk about?

  • The problem
  • What worked for me
  • Mistakes

Why?

  • Long components
  • No layer overlap
  • Time spend testing Manually
  • Merge all of them

Long components

export const LongComponent: React.FC<{}> = () => { 
  
  const [categories] = React.useState<string[]>([]);
  const [videos, setVideos] = React.useState<Video[]>([]);
  const [clasifiedVideos, setClasifiedVideos] = React.useState<ClasifiedVideos>({});
  const [selectedVideo, setSelectedVideo] = React.useState<Video>({});
  const [commentary, setCommentary] = React.useState<string>('');

  React.useEffect( () => {
    setVideos([
      // ...videos retrieved from api
    ]);

    setClasifiedVideos( 
      categories.reduce((total, category) => {
        const foundedVideosOnCategory = videos.filter((video) => 
        	category === video.category
        );
        return {...total, [category]: foundedVideosOnCategory}
      }, {})
    );

  }, []);

  const saveCommentary = () => {
    // ... do something
  }

  return (
  // see right
)};
<div className="LongComponent">
    <section>
    <ul>
      {categories.map((category) => (  
        <li>
          { category }
          <ul>
            { clasifiedVideos[category].map( (video) => (
              <li 
              	onClick={() => setSelectedVideo(video)}
              >{ video.title }</li>)
            )}
          </ul>
        </li>
      ))}
    </ul>
    </section>
    <form>
      <video></video>
      <ul>
        {selectedVideo.comentaries.map((commentary) => (
          <li>{commentary}</li>
        ))}
      </ul>
      <textarea onChange={(event) => {
      	setCommentary(event.target.value)
      }}>{commentary}</textarea>
      <button onClick={saveCommentary}></button>
    </form>
  </div>

Long components

But we want:

  • To understand what we are seeing.
  • Make easy to maintain it by other developers.
  • We want to make changes without risk.
  • We want to delegate responsabilities.
  • Have a good performance

We don't want to reuse it.

Layer overlap

export const LayerOverlap: React.FC<{}> = () => {
  const END_POINT = 'http://localhost:8080/videos';
  //... some useStates
  
  const getVideos = async () => {
    const response = await fetch(END_POINT);
    setVideos(await response.json());
  };

  React.useEffect(() => {
    getVideos().then();
  }, []);

  React.useEffect( () => {
    setClassifiedVideos(
      categories.reduce((total, category) => {
        const foundedVideosOnCategory = videos.filter((video) => category === video.category);
        return {...total, [category]: foundedVideosOnCategory};
      }, {}),
    );
  }, [categories, videos]);

  const saveCommentary = async () => {
    await fetch(`${END_POINT}/${selectedVideo.id}/comments`, {
      method: 'post',
      headers: {'Content-Type': 'application/json'},
      body: newCommentary,
    });
  };

  return (/* Irrelevant content */);
};

Layer overlap

Overlaped Component:

Single Responsability:

My Solution

What is TDD?

Write a test,

watch it fail.

Write just enough code to pass the test

Improve the code wothout changing its behavior

TDD

Test

Code 

Refactor

Classic way:

TDD way:

Why we use it?

Fast Feedback

High code coverage

Quality assurance

Other way to document

It's necessary?

  • expect(true).toBeTruthy()
  • You don't trust in your libs
  • I'm faster without TDD
  • It's imposible to test the unexistent
  • My client doesn't want it!!!

Propably you have lisened something like...

This isn't about TDD,

it's about the way you test 

  • How much time do you expend finding  bugs?
  • Are you testing code or features?
  • Do you tell your surgeon how to do it?

What is the purpouse?

It's not about testing

It's all about developing in small steps

Using the tests as tools

Before starting

let's talk about concepts

Tests Types

  • e2e
  • Unitary
  • Integration

That I ussually use

Tests Types

  • e2e

That I ussually use

Expects are use cases

Clone of real environment

Too expensive

Real coverage

Tests Types

  • Unitary

That I ussually use

Check concrete behaviors

Specific configurations are not required

Faster and cheaper

Local component coverage

Tests Types

  • Integration

That I ussually use

Check behaviors with broader scope

Requires mocks to set limits

Perfect balance

Important coverage but not always real

Tests Types

  • e2e
  • Unitary
  • Integration

That I ussually use

IMPORTANT!!!

You need an architecture!!

References for Architecture

Books:

Videos:

The Approach

OUTSIDE-IN

INSIDE-OUT

Emerging architecture

Componentisation

Predefined architecture

Fuzzy components

London

Chicago

Testing Concepts

describe('Big block', () => {
  it('The functionality', () => {
    
  });
});

Scope

Testing Concepts

it('should have an expect inside', () => {
  expect(true).toBeTruthy(); // OK
  expect(false).toBeTruthy(); // KO

  expect(1).toBe(1); // OK
  expect(1).toBe(2); // KO
  
  expect('Hello').toBe('Hello'); // KO
  expect('Hello').toEqual('Hello'); // OK
});

Matchers

Testing Concepts

describe('Big block', () => {
  beforeEach(() => {
    // Execute this code before each it
  });
  
  afterEach(() => {
    // Execute this code after each it
  });
  
  it('Some test', () => { });
  it('Other test', () => { });
});

Utils

Testing Concepts

Mock

It has a very complex definition

Replace a real function or var, with other that we want

Using Jest:

const foo = jest.fn();
const hello = 'Hello JsDayCan!';

foo(hello);

expect(foo).toHaveBeenCalled();
expect(foo).toHaveBeenCalledWith(hello);
const hello = 'Hello JsDayCan!';
const bye = 'Happy coding!';

const foo = jest.fn( (message) => bye);

const response = foo(hello);

expect(foo).toHaveBeenCalled();
expect(foo).toHaveBeenCalledWith(hello);
expect(response).toEqual(bye);

Testing Concepts

Very helpfull!!

// 1
it('should display all categories', () => {

  
  
  
  expect(foundCategories).toEqual(categories.length);
});
// 2
it('should display all categories', () => {

  
  
  
  expect(foundCategories).toEqual(categories.length); // But... what is it?
});
// 3
it('should display all categories', () => {
  const categories = ['education', 'terror', 'technology'];
  
  
  const foundCategories; // ?
  expect(foundCategories).toEqual(categories.length);
});
// 4
it('should display all categories', () => {
  const categories = ['education', 'terror', 'technology'];
  
  
  const foundCategories = getCategories(/* need CategoryRepository*/);
  expect(foundCategories).toEqual(categories.length);
});
// 5
it('should display all categories', () => {
  const categories = ['education', 'terror', 'technology'];
  const categoryRepository = new CategoryRepository();
  
  const foundCategories = getCategories(categoryRepository); // it calls DB
  expect(foundCategories).toEqual(categories.length);
});
// 6
it('should display all categories', () => {
  const categories = ['education', 'terror', 'technology'];
  const categoryRepository = new CategoryRepository();
  categoryRepository.getAll = jest.fn(() => categories); // mock the result
  const foundCategories = getCategories(categoryRepository);
  expect(foundCategories).toEqual(categories.length);
});

Go from expect to definition

Black-box

SCHRÖDINGER CAT

Testign Concepts

REAL USE CASE

Define use cases

  • We want to display a list of videos ordered by category.

Display categorized videos

  • We want to pick a video and display it and its comments.

Select video

Add comments

  • Can add new comment and response other comments

Define use cases

Define use cases

Define use cases

This tests take a lot of time to execute

So it's very important to be very specific

Conclusions

Emergent Design

How can we do it

Higher Order Components

TIP: The goal is not to break the app

TIP: Take small steps

Remember true === true? 

Let's use our favorites tools to automate work

C:\Users\adria\IdeaProjects\jsday-2k19>npm run new-fc

> react-boilerplate@0.1.0 new-fc C:\Users\adria\IdeaProjects\jsday-2k19
> hygen fc new

√ What path do you want? · containers
√ What name do you want? · VideosNavbar

Loaded templates: C:\Users\adria\IdeaProjects\jsday-2k19\_templates
       added: src/containers/VideosNavbar/VideosNavbar.tsx
       added: src/containers/VideosNavbar/index.ts
       added: src/containers/VideosNavbar/VideosNavbar.spec.tsx
       added: src/containers/VideosNavbar/VideosNavbar.stories.tsx
       added: src/containers/VideosNavbar/VideosNavbar.scss

Later we can decide if we should remove it

Emergent Design

// 1

describe('VideosNavbar', () => {
  it('should display the default message', () => {
    const { queryByText } = render(
      <VideosNavbar/>,
    );
    expect(
      queryByText('Hello from VideosNavbar!')
    ).toBeTruthy();
  });
});
// 1

export const VideosNavbar: React.FC<{}> = () => (
  <nav className="VideosNavbar">
    Hello from VideosNavbar!
  </nav>
);
// 2

describe('CategoryTitle', () => {
  it('should display the default message', () => {
    
    const { queryByText } = render(
      <CategoryTitle/>,
    );
    expect(
      queryByText('Hello from CategoryTitle!')
    ).toBeTruthy();
  });
});
// 2

export const CategoryTitle: React.FC<{}> = 
  () => (
  <div className="CategoryTitle">
    Hello from CategoryTitle!
  </div>
);
// 3

describe('CategoryTitle', () => {
  it('should display the default message', () => {
    const aCategoryTitle = 'Irrelevant title';
    const { queryByText } = render(
      <CategoryTitle>{aCategoryTitle}</CategoryTitle>,
    );
    expect(
      queryByText(aCategoryTitle)
    ).toBeTruthy();
  });
});
// 4

export const CategoryTitle: React.FC<{}> = 
  ({...props}) => (
  <h3 className="CategoryTitle" {...props} />
);
// 3

describe('CategoryTitle', () => {
  it('should display the default message', () => {
    const aCategoryTitle = 'Irrelevant title';
    const { queryByText } = render(
      <CategoryTitle>{aCategoryTitle}</CategoryTitle>,
    );
    expect(
      queryByText(aCategoryTitle)
    ).toBeTruthy();
  });
});

Refactor?
Storybook?
Sass?

CategoryTitle

Emergent Design

VideoItem

// 1

export const VideosNavbar: React.FC<{}> = () => (
  <nav className="VideosNavbar">
    Hello from VideosNavbar!
  </nav>
);
// 1
describe('VideoItem', () => { //Same as Category
  it('should display the default message', () => {
    const selectAVideo = jest.fn();
    const aVideoName = 'irrelevant video name';
    const { getByText, queryByText } = render(
      <VideoItem onClick={selectAVideo}>{aVideoName}</VideoItem>,
    );
    expect(queryByText(aVideoName)).toBeTruthy();
  });

  

});
// 2
describe('VideoItem', () => {
  it('should display the default message', () => {...});

  it('should do something when is clicked', () => {
    const selectAVideo = jest.fn();
    const aVideoName = 'irrelevant video name';
    const { getByText, queryByText } = render(
      <VideoItem onClick={selectAVideo}>{aVideoName}</VideoItem>,
    );
    fireEvent.click(getByText(aVideoName));
    expect(selectAVideo).toHaveBeenCalled();
  });
});
export const VideoItem: React.FC<VideoItemProps> = 
  ({children}) => (
  <div 
  	className="VideoItem" 
  	
  >
    {children}
  </div>
);
export const VideoItem: React.FC<VideoItemProps> = 
  ({onClick, children}) => (
  <div 
  	className="VideoItem" 
  	onClick={(event) => {onClick(); }}
  >
    {children}
  </div>
);

Emergent Design

VideoItem - Refactoring

// 1
describe('VideoItem', () => {
  it('should display the default message', () => {
    const selectAVideo = jest.fn();
    const aVideoName = 'irrelevant video name';
    const { getByText, queryByText } = render(
      <VideoItem onClick={selectAVideo}>
       {aVideoName}
      </VideoItem>,
    );
    expect(queryByText(aVideoName)).toBeTruthy();
  });

  it('should do something when is clicked', () => {
    const selectAVideo = jest.fn();
    const aVideoName = 'irrelevant video name';
    const { getByText, queryByText } = render(
      <VideoItem onClick={selectAVideo}>
       {aVideoName}
      </VideoItem>,
    );
    fireEvent.click(getByText(aVideoName));
    expect(selectAVideo).toHaveBeenCalled();
  });
});
// 2
describe('VideoItem', () => {
  let selectAVideo: () => void;
  let aVideoName: string;
  let videoItem: RenderResult;

  beforeEach(() => {
    selectAVideo = jest.fn();
    aVideoName = 'irrelevant video name';
    videoItem = render(
      <VideoItem onClick={selectAVideo}>
        {aVideoName}
      </VideoItem>,
    );
  });

  it('should display the default message', () => {
    expect(videoItem.queryByText(aVideoName)).toBeTruthy();
  });

  it('should do something when is clicked', () => {
    fireEvent.click(videoItem.getByText(aVideoName));
    expect(selectAVideo).toHaveBeenCalled();
  });
});

Emergent Design

All together

// 1

describe('VideosNavbar', () => {
  it('should display the default message', () => {
    const { queryByText } = render(
      <VideosNavbar/>,
    );
    expect(
      queryByText('Hello from VideosNavbar!')
    ).toBeTruthy();
  });
});
// 1

export const VideosNavbar: 
  React.FC<{}> = () => (
  <nav className="VideosNavbar">
    Hello from VideosNavbar!
  </nav>
);
// 2

describe('VideosNavbar', () => {
  let categories: string [];
  let videos: Video[];

  beforeEach(() => {
    categories = [];
    videos = [];
  });

  it('should display all the videos', () => {
    const properties = {categories, videos}
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );
    expect(
      renderResult.getByTestId('VideosNavbar')
    ).toBeTruthy();
  	// expect(foundVideos.length).toBe(videos.length);
  });
});
// 3

interface VideosNavbarProps {
  videos: Video [];
  categories: string[];
}

export const VideosNavbar: 
  React.FC<VideosNavbarProps> 
  = ({videos, categories}) => (
  <nav className="VideosNavbar" 
    data-testid="VideosNavbar">
    Hello from VideosNavbar!
  </nav>
);

It should be done in only one step

// 4

describe('VideosNavbar', () => {
  let categories: string [];
  let videos: Video[];

  beforeEach(() => {
    categories = [];
    videos = [];
  });

  it('should display all the videos', () => {
    const properties = {categories, videos}
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );
  
    const foundVideos = renderResult
      .queryAllByTestId('video-item', { exact: false });
    expect(foundVideos.length).toBe(videos.length);
  });
});
// 2

describe('VideosNavbar', () => {
  let categories: string [];
  let videos: Video[];

  beforeEach(() => {
    categories = [];
    videos = [];
  });

  it('should display all the videos', () => {
    const properties = {categories, videos}
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );
    expect(
      renderResult.getByTestId('VideosNavbar')
    ).toBeTruthy();
  	// expect(foundVideos.length).toBe(videos.length);
  });
});

Why it's green

Is that the expected behaviour

Emergent Design

All together

// 1

it('should display a message when list of videos is empty', () => {...});

it('should display all the videos', () => {
  const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '1'}
  const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '2'}
  videos = [aVideo1, aVideo2]
  const properties = {categories, videos};
  const renderResult: RenderResult = render(
    <VideosNavbar {...properties}/>,
  );

  const foundVideos = renderResult
    .queryAllByTestId('video-item', { exact: false });
  expect(foundVideos.length).toBe(videos.length);
});
interface VideosNavbarProps {
  videos: Video [];
  categories: string[];
}

export const VideosNavbar: React.FC<VideosNavbarProps> = 
  ({videos, categories}) => (
  <nav className="VideosNavbar" data-testid="VideosNavbar">
    {videos && videos.length === 0 && 'Not videos found'}
  </nav>
);

VideosNavbar.displayName = 'VideosNavbar';
// 2

it('should display a message when list of videos is empty', () => {...});

it('should display all the videos', () => {
  const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '1'}
  const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: 'irrelevant', id: '2'}
  videos = [aVideo1, aVideo2]
  const properties = {categories, videos};
  const renderResult: RenderResult = render(
    <VideosNavbar {...properties}/>,
  );

  const foundVideos = renderResult
    .queryAllByTestId('video-item', { exact: false });
  expect(foundVideos.length).toBe(videos.length);
});
interface VideosNavbarProps {...}

export const VideosNavbar: React.FC<VideosNavbarProps> 
  = ({videos, categories}) => {

  const handleClick = () => {
    throw new Error('Uninplemented method');
  };

  return (
    <nav className="VideosNavbar" data-testid="VideosNavbar">
      {videos && videos.length === 0 && 'Not videos found'}
      {videos.map((video) =>
        <VideoItem
          key={video.id}
          onClick={handleClick}
          data-testid="video-item"/>,
      )}
    </nav>
  );
};

...Same with categories

Emergent Design

All together

Okey... but we want to group videos by category...

it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
    const aVideo2: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
    const aVideo3: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = []; // ?
    const videosInAction = []; // ?
    expect(videosInTerror.length).toBe(1);
    expect(videosInAction.length).toBe(2);
    // what happen in videos without category? -> new test
  });

Emergent Design

All together

it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
    const aVideo2: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
    const aVideo3: Video = 
          {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = renderResult
      .getByTestId('category-block-terror')
      .querySelectorAll('[data-testid=\'video-item\']');
    const videosInAction = renderResult
      .getByTestId('category-block-action')
      .querySelectorAll('[data-testid=\'video-item\']');
    expect(videosInTerror.length).toBe(1);
    expect(videosInAction.length).toBe(2);
    // what happen in videos without category? -> new test
  });

Emergent Design

All together - Result

export const VideosNavbar: React.FC<VideosNavbarProps> = ({videos, categories}) => {

  const [classifiedVideos, setClassifiedVideos] = React.useState({} as ClassifiedVideos);

  const isVideoOnCategory = (category: string) =>
    (video: Video) => category === video.category;

  React.useEffect( () => {
    setClassifiedVideos(
      categories.reduce((total, category) => {
        const foundedVideosOnCategory = videos
          .filter(isVideoOnCategory(category));
        return {
          ...total, 
          [category]: foundedVideosOnCategory
        };
      }, {}),
    );
  }, []);

  const handleClick = () => {
    return new Error('Uninplemented method');
  };
  
  return (...)
};
return (
  <nav className="VideosNavbar" data-testid="VideosNavbar">
    {videos && videos.length === 0 && 'Not videos found'}
    {categories.map((category, index) =>
      <section key={`category-block-${category}`}
               data-testid={`category-block-${category}`}>
        <CategoryTitle data-testid={`category-title-${index}`}>
          {category}</CategoryTitle>
        {classifiedVideos[category] 
		&& classifiedVideos[category].map( (video) =>
          <VideoItem
            key={video.id}
            onClick={handleClick}
            data-testid="video-item"/>,
        )}
      </section>)}
  </nav>
);

Emergent Design

It works!

But... it's a bit dirty

We can use helpers to make it more redeable

it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: terrorCategory, id: '1'};
    const aVideo2: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '2'};
    const aVideo3: Video = {title: 'irrelevant', url: 'irrelevant', comments: [], category: actionCategory, id: '3'};
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = renderResult
      .getByTestId('category-block-terror')
      .querySelectorAll('[data-testid=\'video-item\']');
    const videosInAction = renderResult
      .getByTestId('category-block-action')
      .querySelectorAll('[data-testid=\'video-item\']');
    expect(videosInTerror).toBe(1);
    expect(videosInAction).toBe(2);
    // what happen in videos without category? -> new test
  });
const buildVideoWidth = ({
  id = 'irrelevant',
  title = 'irrelevant',
  category = 'irrelevant',
  url = 'irrelevant',
  comments= [],
}): Video => ({
  id,
  title,
  category,
  url,
  comments,
});
it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
    const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
    const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = renderResult
      .getByTestId('category-block-terror')
      .querySelectorAll('[data-testid=\'video-item\']');
    const videosInAction = renderResult
      .getByTestId('category-block-action')
      .querySelectorAll('[data-testid=\'video-item\']');
    expect(videosInTerror).toBe(1);
    expect(videosInAction).toBe(2);
    // what happen in videos without category? -> new test
  });

It works!

But... it's a bit dirty

it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
    const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
    const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = renderResult
      .getByTestId('category-block-terror')
      .querySelectorAll('[data-testid=\'video-item\']');
    const videosInAction = renderResult
      .getByTestId('category-block-action')
      .querySelectorAll('[data-testid=\'video-item\']');
    expect(videosInTerror).toBe(1);
    expect(videosInAction).toBe(2);
    // what happen in videos without category? -> new test
  });
const findVideosInCategory = (
  renderResult: RenderResult,
  categoryBlockTestId: string) =>
  renderResult
  .getByTestId(categoryBlockTestId)
  .querySelectorAll(
    '[data-testid=\'video-item\']'
  );
it('should display the videos classified by categories', () => {
    const terrorCategory = 'terror';
    const actionCategory = 'action';
    const aVideo1: Video = buildVideoWidth({id : '1', category: terrorCategory});
    const aVideo2: Video = buildVideoWidth({id : '2', category: actionCategory});
    const aVideo3: Video = buildVideoWidth({id : '3', category: actionCategory});
    categories = [terrorCategory, actionCategory];
    videos = [aVideo1, aVideo2, aVideo3];
    const properties = {categories, videos};
    const renderResult: RenderResult = render(
      <VideosNavbar {...properties}/>,
    );

    const videosInTerror = findVideosInCategory(renderResult, 'category-block-terror');
    const videosInAction = findVideosInCategory(renderResult, 'category-block-action');

    expect(videosInTerror).toBe(1);
    expect(videosInAction).toBe(2);
    // what happen in videos without category? -> new test
  });

Conclusions

  • Working code at all times
  • Overengineering avoidance
  • Discover new cases

TDD

Test

Code 

Refactor

Emergent Design

Delay decisions

Now we have to retrieve the data... but where are they?

  • API REST using fetch?
  • GraphQL using a client?
  • Local database?
  • Firebase?

We don't have to decide it yet

We can continue developing and make the desicion later

How?

Let's create a test

it('should get the videos when component is loaded', () => {
  // type is not defined
  const videosRepository: VideosRepository;
  expect(videosRepository.getVideos)
    .toHaveBeenCalledTimes(1);
});

And imagine what is the perfect api that we want

export interface VideosRepository {
  getVideos: () => Promise<Video[]>;
}
it('should get the videos when component is loaded', () => {
  const videosRepository: VideosRepository = {
    getVideos: jest.fn(() => Promise.resolve([]),
  };
  expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
});

Delay decisions

it('should get the the videos when component is loaded', 
  () => {
  const videosRepository: VideosRepository = {
    getVideos: jest.fn(
      () => Promise.resolve([])
    ),
  };

  const renderResult: RenderResult = 
  render(
    <Videos 
      dependencies={{videosRepository}}
    />,
  );
  expect(videosRepository.getVideos)
    .toHaveBeenCalledTimes(1);
});
// 1






export const Videos: React.FC<{}> = () => {
  
  
  
    
  
  
  
  const [selectedVideo, setSelectedVideo] = useState(null);
  const handleSave = () => {
    throw new Error('Not implemented');
  };
  return <section className="Videos">
    <VideosNavbar videos={[]} categories={[]}/>
    <VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
  </section>;
};
// 2
interface VideosProps {
  dependencies: {
    videosRepository: VideosRepository,
  };
}

export const Videos: React.FC<VideosProps> = ({dependencies}) => {
  const { videosRepository } = dependencies;
  
  
  
  
  
  
  const [selectedVideo, setSelectedVideo] = useState(null);
  const handleSave = () => {
    throw new Error('Not implemented');
  };
  return <section className="Videos">
    <VideosNavbar videos={[]} categories={[]}/>
    <VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
  </section>;
};
// 3
interface VideosProps {
  dependencies: {
    videosRepository: VideosRepository,
  };
}

export const Videos: React.FC<VideosProps> = ({dependencies}) => {
  const { videosRepository } = dependencies;
  const [videos, setVideos] = React.useState([] as Video[]);

  React.useEffect(() => {
    videosRepository.getVideos().then(setVideos);
  }, []);
  
  const [selectedVideo, setSelectedVideo] = useState(null);
  const handleSave = () => {
    throw new Error('Not implemented');
  };
  return <section className="Videos">
    <VideosNavbar videos={videos} categories={[]}/>
    <VideoDetails selectedVideo={selectedVideo} onSave={handleSave}/>
  </section>;
};

Delay decisions

In our App we will initialize all our dependencies and pass them in to the rest of the application

const dependencies = {
    videosRepository: { getVideos: () => Promise.resolve([])},
  }

  return (
    <div className="App">
      <header className="App-header" >
        My videos
      </header>
      <section className="App-content">
        <Videos dependencies={dependencies}/>
      </section>
    </div>
  );
const dependencies = {
    videosRepository: new VideosRepository(),
  }

  return (
    <div className="App">
      <header className="App-header" >
        My videos
      </header>
      <section className="App-content">
        <Videos dependencies={dependencies}/>
      </section>
    </div>
  );

Delay decisions

We can make it more complex by adding more dependencies:

describe('Videos', () => {

  let dependencies: AppDependencies;
  let videosRepository: VideosRepository;
  let categoriesRepository: CategoriesRepository;
  beforeEach(() => {
    videosRepository = { getVideos: jest.fn(() => Promise.resolve([])), };
    categoriesRepository = { getCategories: jest.fn(() => Promise.resolve([])), };
    dependencies = { videosRepository, categoriesRepository};
  });

  it('should get the starting values when component is loaded', () => {
    const renderResult: RenderResult = render(
      <Videos dependencies={dependencies}/>,
    );
    expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
    expect(categoriesRepository.getCategories).toHaveBeenCalledTimes(1);
  });
});

Delay decisions

Or we can work with real values and mock it:

describe('Videos', () => {

  let dependencies: AppDependencies;
  beforeEach(() => {
    dependencies = initializeDependencies();
    dependencies.videosRepository.getVideos = jest.fn(() => Promise.resolve([]));
    dependencies.categoriesRepository.getCategories = jest.fn(() => Promise.resolve([]));
  });

  it('should get the starting values when component is loaded', () => {
    const {videosRepository, categoriesRepository} = dependencies;
    const renderResult: RenderResult = render(
      <Videos dependencies={dependencies}/>,
    );
    expect(videosRepository.getVideos).toHaveBeenCalledTimes(1);
    expect(categoriesRepository.getCategories).toHaveBeenCalledTimes(1);
  });
});

Delay decisions

It's very IMPORTANT to test the Promise.rejected too

More than the success case

Delay decisions

Conclusions

  • Postpone decisions
  • Economic tests for real cases
  • Check the integration
  • This is not a truly black-box, but feels like it is

Delay decisions

Some warnings

What happen with bugs?

If we found a bug when we are coding

We should create a new cases reproduce it

And then fix it

And Redux?

How can I do it?

You can test your parent component like this

describe('VideoItem', () => {

  let dependencies: AppDependencies;
  let store: Store;

  describe('Scope 1', () => {
	before(() => {
      //...
	});

	it('should load the user information', () => {...});
	it('should save the user information', () => {...});

  });
  
  describe('Scope 2', () => {...});
});
  
const dependencies: AppDependencies = initializeDependencies();
      
  // Override dependencies when components mount
dependencies.videosRepository.getVideos = () => Promise.resolve([]);
store = initializeStore():
      
const parentContainer = 
  render(
    <Provider store={store}>
      <Videos dependencies={dependencies}/>
    </Provider>,
  );

Third party libraries

Evolution Problem

React Hooks

React

evolves

Enzyme

React-testing-library

We made a mistake by not wrapping up our libs

Wrap it!!

Check the coverage

To discover new cases

 PASS  src/components/Commment/Commment.spec.tsx
-------------------|----------|----------|----------|----------|-------------------|
File               |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files          |    96.87 |    96.87 |    93.75 |    96.87 |                   |
 App.tsx           |      100 |      100 |      100 |      100 |                   |
 CategoryTitle.tsx |      100 |      100 |      100 |      100 |                   |
 Comments.tsx      |      100 |      100 |      100 |      100 |                   |
 ReplyComment.tsx  |      100 |      100 |      100 |      100 |                   |
 VideoDetails.tsx  |       75 |    83.33 |       50 |       75 |                19 |
 VideoItem.tsx     |      100 |      100 |      100 |      100 |                   |
 Videos.tsx        |      100 |      100 |      100 |      100 |                   |
 VideosNavbar.tsx  |      100 |      100 |      100 |      100 |                   |
-------------------|----------|----------|----------|----------|-------------------|

Test Suites: 8 passed, 8 total
Tests:       15 passed, 16 total
Snapshots:   0 total
Time:        4.86s
Ran all test suites.

Posible or Imposible?

Conclusions

  • It's efficent
  • Powerfull tool
  • Easy for other developers

Positive

Negative

  • Requires knowledge
  • Requires practice

Questions?

Thank you!!

Made with Slides.com